<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>后台管理 · 武工研发部门</title>
  <style>
    :root {
      color-scheme: dark;
      --bg: #0b0c10;
      --panel: rgba(22, 28, 44, 0.75);
      --border: rgba(255, 255, 255, 0.08);
      --text: #e6e9ef;
      --text-soft: rgba(230, 233, 239, 0.7);
      --primary: #2b63ff;
      --danger: #ff5f5f;
      --radius: 18px;
      --space: 1.2rem;
      font-family: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
    }
    body {
      margin: 0;
      min-height: 100vh;
      background: radial-gradient(circle at top, rgba(22,93,255,0.22), transparent 55%), var(--bg);
      color: var(--text);
    }
    header {
      padding: 1.5rem 2rem;
      border-bottom: 1px solid var(--border);
      display: flex;
      align-items: center;
      justify-content: space-between;
      backdrop-filter: blur(14px);
      background: rgba(11, 12, 16, 0.65);
    }
    header h1 {
      margin: 0;
      font-size: 1.25rem;
      letter-spacing: 0.04em;
    }
    .admin-layout {
      box-sizing: border-box;
      max-width: 1280px;
      margin: 0 auto;
      padding: 2.5rem clamp(1.25rem, 4vw, 4rem);
      display: flex;
      align-items: flex-start;
      gap: 2rem;
    }
    .admin-sidebar {
      width: 240px;
      flex-shrink: 0;
      display: flex;
      flex-direction: column;
      gap: 0.75rem;
      border: 1px solid var(--border);
      background: rgba(14, 20, 34, 0.7);
      border-radius: var(--radius);
      padding: 1rem;
      box-shadow: 0 18px 40px rgba(0, 0, 0, 0.35);
      backdrop-filter: blur(12px);
    }
    .admin-content {
      flex: 1;
      display: grid;
      gap: 2rem;
      min-width: 0;
    }
    section {
      border: 1px solid var(--border);
      background: var(--panel);
      border-radius: var(--radius);
      padding: 1.6rem clamp(1.5rem, 3vw, 2.6rem);
      backdrop-filter: blur(12px);
      box-shadow: 0 18px 40px rgba(0, 0, 0, 0.35);
    }
    section h2 {
      margin: 0 0 1.1rem;
      font-size: 1.1rem;
      display: flex;
      align-items: center;
      gap: 0.75rem;
      letter-spacing: 0.05em;
      text-transform: uppercase;
      color: var(--primary);
    }
    label {
      display: grid;
      gap: 0.4rem;
      font-size: 0.85rem;
      color: var(--text-soft);
    }
    input[type="text"],
    input[type="number"],
    input[type="url"],
    input[type="password"],
    textarea,
    select {
      background: rgba(11, 14, 24, 0.8);
      border: 1px solid var(--border);
      color: var(--text);
      padding: 0.65rem 0.75rem;
      border-radius: 12px;
      font-size: 0.95rem;
    }
    textarea {
      min-height: 120px;
      resize: vertical;
      font-family: "Fira Code", Consolas, monospace;
    }
    button {
      background: var(--primary);
      color: white;
      border: none;
      border-radius: 12px;
      padding: 0.65rem 1.4rem;
      font-weight: 600;
      cursor: pointer;
      letter-spacing: 0.05em;
      transition: transform 0.18s ease, box-shadow 0.18s ease;
    }
    button:hover {
      transform: translateY(-1px);
      box-shadow: 0 10px 22px rgba(43, 99, 255, 0.36);
    }
    .admin-tab {
      border: 1px solid rgba(255, 255, 255, 0.06);
      background: rgba(17, 23, 38, 0.75);
      color: var(--text);
      border-radius: 12px;
      padding: 0.8rem 1rem;
      display: flex;
      justify-content: space-between;
      align-items: center;
      font-size: 0.88rem;
      font-weight: 600;
      letter-spacing: 0.04em;
      cursor: pointer;
      transition: background 0.2s ease, box-shadow 0.2s ease;
    }
    .admin-tab:hover {
      background: rgba(31, 42, 68, 0.85);
      box-shadow: 0 10px 22px rgba(43, 99, 255, 0.18);
    }
    .admin-tab:focus-visible {
      outline: 2px solid rgba(43, 99, 255, 0.6);
      outline-offset: 2px;
    }
    .admin-tab.active {
      border-color: rgba(43, 99, 255, 0.45);
      background: rgba(38, 53, 96, 0.9);
      box-shadow: 0 12px 28px rgba(43, 99, 255, 0.25);
      color: #fff;
    }
    @media (max-width: 768px) {
      .admin-layout {
        flex-direction: column;
        gap: 1.5rem;
      }
      .admin-sidebar {
        width: 100%;
        flex-direction: row;
        gap: 0.6rem;
        padding: 0.75rem;
        overflow-x: auto;
        box-sizing: border-box;
      }
      .admin-tab {
        flex: 0 0 auto;
        white-space: nowrap;
        min-width: 140px;
      }
    }
    .danger {
      background: var(--danger);
    }
    .grid {
      display: grid;
      gap: 1.1rem;
    }
    .card {
      border: 1px solid var(--border);
      background: rgba(14, 20, 34, 0.8);
      border-radius: 16px;
      padding: 1.1rem;
      display: grid;
      gap: 0.8rem;
    }
    .toolbar {
      display: flex;
      gap: 0.8rem;
      flex-wrap: wrap;
      margin-top: 1rem;
    }
    .preview-banner {
      display: flex;
      align-items: center;
      gap: 0.8rem;
      padding: 0.75rem 0.9rem;
      margin-top: 0.75rem;
      border: 1px solid rgba(22, 93, 255, 0.35);
      border-radius: 10px;
      background: rgba(22, 93, 255, 0.12);
      color: var(--text-soft);
      flex-wrap: wrap;
    }
    .preview-banner .preview-message {
      flex: 1 1 auto;
      min-width: 200px;
    }
    .preview-banner strong {
      color: var(--text-primary);
    }
    .preview-banner code {
      font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
      background: rgba(8, 12, 24, 0.45);
      padding: 0.2rem 0.45rem;
      border-radius: 6px;
      font-size: 0.85rem;
      word-break: break-all;
      flex: 1 1 100%;
    }
    .preview-banner button {
      flex: 0 0 auto;
    }
    .status {
      font-size: 0.8rem;
      color: var(--text-soft);
      margin-left: auto;
    }
    details.page-editor {
      border: 1px solid var(--border);
      background: rgba(14, 20, 34, 0.75);
      border-radius: 16px;
      padding: 1.1rem;
    }
    details.page-editor summary {
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: space-between;
      font-weight: 600;
      margin-bottom: 1rem;
      list-style: none;
    }
    details.page-editor[open] summary {
      margin-bottom: 1rem;
    }
    summary::-webkit-details-marker {
      display: none;
    }
    .muted {
      color: var(--text-soft);
      font-size: 0.8rem;
    }
    .inline {
      display: flex;
      gap: 1rem;
      align-items: center;
    }
    .theme-grid {
      display: grid;
      gap: 1.2rem;
      grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    }
    .theme-group {
      display: grid;
      gap: 0.9rem;
      padding: 1rem;
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 16px;
      background: rgba(10, 14, 24, 0.6);
    }
    .theme-group h3 {
      margin: 0;
      font-size: 0.85rem;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: var(--text-soft);
    }
    .color-field {
      display: grid;
      gap: 0.4rem;
    }
    .color-field span {
      font-size: 0.8rem;
      letter-spacing: 0.04em;
    }
    .color-control {
      display: flex;
      align-items: center;
      gap: 0.6rem;
    }
    .color-control input[type="color"] {
      width: 44px;
      height: 32px;
      padding: 0;
      border: none;
      border-radius: 8px;
      background: transparent;
      cursor: pointer;
    }
    .color-value {
      flex: 1;
      font-family: "Fira Code", Consolas, monospace;
      text-transform: uppercase;
    }
    .color-value.is-invalid {
      border-color: var(--danger);
      box-shadow: 0 0 0 1px var(--danger);
    }
    .theme-group--gradients .helper-text {
      margin: -0.2rem 0 0;
    }
    .helper-text.small {
      font-size: 0.72rem;
      margin: -0.1rem 0 0;
    }
    .surface-gradient-panel {
      display: grid;
      gap: 1rem;
    }
    .surface-gradient-item {
      border: 1px solid var(--border);
      background: rgba(14, 20, 34, 0.55);
      border-radius: 14px;
      padding: 1.05rem 1.2rem;
      display: grid;
      gap: 0.9rem;
      transition: border-color 0.2s ease, background 0.2s ease;
    }
    .surface-gradient-item.is-disabled {
      opacity: 0.55;
    }
    .surface-gradient-item__header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 0.6rem;
    }
    .surface-gradient-toggle {
      display: inline-flex;
      align-items: center;
      gap: 0.45rem;
      font-size: 0.8rem;
      color: var(--text-soft);
    }
    .surface-gradient-toggle input[type="checkbox"] {
      width: 18px;
      height: 18px;
      margin: 0;
      cursor: pointer;
      accent-color: var(--primary);
    }
    .surface-gradient-item__body {
      display: grid;
      gap: 0.75rem;
    }
    .surface-gradient-angle-field {
      display: grid;
      gap: 0.4rem;
    }
    .surface-gradient-angle-input {
      width: 100%;
      background: rgba(11, 14, 24, 0.8);
      border: 1px solid var(--border);
      color: var(--text);
      padding: 0.65rem 0.75rem;
      border-radius: 12px;
      font-size: 0.95rem;
      appearance: textfield;
    }
    .surface-gradient-angle-input::-webkit-inner-spin-button,
    .surface-gradient-angle-input::-webkit-outer-spin-button {
      margin: 0;
    }
    .surface-gradient-angle-input:focus-visible {
      border-color: var(--primary);
      box-shadow: 0 0 0 3px rgba(22, 93, 255, 0.24);
      outline: none;
    }
    .surface-gradient-angle-input.is-invalid {
      border-color: var(--danger);
      box-shadow: 0 0 0 1px var(--danger);
    }
    .surface-gradient-item.is-disabled .surface-gradient-angle-input,
    .surface-gradient-item.is-disabled .color-control input {
      cursor: not-allowed;
    }
    .theme-hint {
      margin: 0 0 1.2rem;
    }
    .gradient-options {
      display: grid;
      gap: 0.9rem;
    }
    .gradient-toggle {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 0.6rem 0.8rem;
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 12px;
      background: rgba(10, 14, 24, 0.6);
    }
    .gradient-toggle span {
      font-size: 0.82rem;
      color: var(--text-soft);
      letter-spacing: 0.04em;
    }
    .gradient-toggle input[type="checkbox"] {
      width: 44px;
      height: 22px;
      accent-color: var(--primary);
      cursor: pointer;
    }
    .gradient-row {
      display: grid;
      gap: 0.9rem;
      grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    }
    .gradient-angle {
      display: grid;
      gap: 0.4rem;
    }
    .gradient-angle input[type="number"] {
      width: 100%;
      appearance: textfield;
    }
    .gradient-angle input[type="number"]::-webkit-inner-spin-button,
    .gradient-angle input[type="number"]::-webkit-outer-spin-button {
      margin: 0;
    }
    .gradient-angle .helper-text {
      margin: 0;
    }
    [data-gradient-field].is-disabled {
      opacity: 0.55;
    }
    [data-gradient-field].is-disabled input {
      cursor: not-allowed;
    }
    .hidden {
      display: none !important;
    }
    .page-editor-body {
      display: grid;
      gap: 1.2rem;
    }
    .page-forms {
      display: grid;
      gap: 1rem;
    }
    .page-card {
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 16px;
      background: rgba(10, 14, 24, 0.65);
      padding: 1.2rem;
      display: grid;
      gap: 0.85rem;
    }
    .page-card h3 {
      margin: 0;
      font-size: 0.9rem;
      letter-spacing: 0.06em;
      text-transform: uppercase;
      color: var(--text-soft);
    }
    .page-card .field-grid {
      display: grid;
      gap: 0.8rem;
    }
    .page-card .field-grid.split {
      grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    }
    .page-card .inline-inputs {
      display: grid;
      gap: 0.6rem;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    }
    .page-card textarea {
      min-height: 90px;
      font-family: inherit;
      resize: vertical;
    }
    .json-card {
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 16px;
      background: rgba(6, 10, 20, 0.7);
      padding: 1.2rem;
      display: grid;
      gap: 0.9rem;
    }
    .json-card summary {
      cursor: pointer;
      font-weight: 600;
      letter-spacing: 0.04em;
    }
    .json-card textarea {
      min-height: 220px;
    }
    .page-hints {
      display: grid;
      gap: 0.6rem;
      padding: 0.8rem 1rem;
      border: 1px solid rgba(255, 255, 255, 0.06);
      border-radius: 14px;
      background: rgba(12, 16, 28, 0.6);
    }
    .page-hints strong {
      font-size: 0.84rem;
      letter-spacing: 0.04em;
    }
    .page-hints ul {
      margin: 0;
      padding-left: 1.1rem;
      display: grid;
      gap: 0.35rem;
      font-size: 0.82rem;
      color: var(--text-soft);
    }
    .sections-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1rem;
    }
    .sections-actions {
      display: flex;
      gap: 0.6rem;
      flex-wrap: wrap;
    }
    .sections-actions select {
      min-width: 150px;
    }
    .sections-list {
      display: grid;
      gap: 0.85rem;
    }
    .section-empty {
      font-size: 0.82rem;
      color: var(--text-soft);
    }
    .section-item {
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 14px;
      background: rgba(7, 11, 20, 0.7);
      padding: 0.2rem 0.8rem 0.9rem;
    }
    .section-item summary {
      display: flex;
      align-items: center;
      gap: 0.75rem;
      cursor: pointer;
      padding: 0.6rem 0;
      font-weight: 600;
      list-style: none;
    }
    .section-item summary::-webkit-details-marker {
      display: none;
    }
    .section-handle {
      width: 1.6rem;
      height: 1.6rem;
      display: grid;
      place-items: center;
      border-radius: 8px;
      background: rgba(255, 255, 255, 0.08);
      font-size: 0.9rem;
      color: var(--text-soft);
      cursor: grab;
      user-select: none;
    }
    .section-handle:active {
      cursor: grabbing;
    }
    .section-summary-title {
      flex: 1;
    }
    .section-type-tag {
      font-size: 0.75rem;
      padding: 0.2rem 0.6rem;
      border-radius: 999px;
      background: rgba(43, 99, 255, 0.18);
      color: var(--primary);
      letter-spacing: 0.05em;
      text-transform: uppercase;
    }
    .section-item.drag-over {
      outline: 2px dashed rgba(22, 93, 255, 0.6);
    }
    .section-buttons {
      display: flex;
      gap: 0.4rem;
    }
    .section-buttons button {
      background: rgba(43, 99, 255, 0.14);
      padding: 0.3rem 0.7rem;
      border-radius: 8px;
      font-size: 0.75rem;
      font-weight: 500;
      letter-spacing: 0.03em;
    }
    .section-buttons button:hover {
      background: rgba(43, 99, 255, 0.24);
    }
    .section-fields {
      display: grid;
      gap: 0.9rem;
      padding-bottom: 0.4rem;
    }
    .section-fields .field-grid {
      display: grid;
      gap: 0.75rem;
    }
    .section-fields .field-grid.split {
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    }
    .section-sublist {
      display: grid;
      gap: 0.65rem;
    }
    .subitem {
      border: 1px solid rgba(255, 255, 255, 0.05);
      border-radius: 12px;
      background: rgba(13, 16, 26, 0.8);
      padding: 0.75rem;
      display: grid;
      gap: 0.6rem;
    }
    .subitem.drag-over {
      outline: 1.5px dashed rgba(22, 93, 255, 0.45);
    }
    .subitem header {
      display: flex;
      align-items: center;
      gap: 0.6rem;
      font-weight: 600;
      font-size: 0.85rem;
      color: var(--text-soft);
    }
    .subitem header .subitem-title {
      flex: 1;
    }
    .subitem header button {
      padding: 0.2rem 0.6rem;
      background: rgba(255, 255, 255, 0.08);
      border-radius: 6px;
      font-size: 0.72rem;
      font-weight: 500;
    }
    .subitem header button:hover {
      background: rgba(255, 255, 255, 0.16);
    }
    .drag-handle {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      padding: 0.2rem 0.45rem;
      border-radius: 6px;
      background: rgba(255, 255, 255, 0.06);
      font-size: 0.72rem;
      color: var(--text-soft);
      cursor: grab;
      user-select: none;
    }
    .drag-handle:active {
      cursor: grabbing;
    }
    .sublist-actions {
      display: flex;
      gap: 0.5rem;
      flex-wrap: wrap;
    }
    .sublist-actions button {
      background: rgba(43, 99, 255, 0.16);
      border-radius: 8px;
      padding: 0.35rem 0.75rem;
      font-size: 0.75rem;
      font-weight: 500;
    }
    .sublist-actions button:hover {
      background: rgba(43, 99, 255, 0.26);
    }
    .helper-text {
      font-size: 0.75rem;
      color: var(--text-soft);
    }
  </style>
</head>
<body>
  <header>
    <h1>武工工业智研院 · 内容后台</h1>
    <div class="inline">
      <label style="margin:0;">
        <span>管理员令牌</span>
        <input type="password" id="adminToken" placeholder="输入后台令牌" />
      </label>
      <button id="btnRemember">记住令牌</button>
    </div>
  </header>

  <div class="admin-layout">
    <aside class="admin-sidebar" role="tablist" aria-label="配置面板">
      <button type="button" class="admin-tab active" id="tab-site" role="tab" aria-selected="true" aria-controls="panel-site" data-target="site" tabindex="0">站点设置</button>
      <button type="button" class="admin-tab" id="tab-theme" role="tab" aria-selected="false" aria-controls="panel-theme" data-target="theme" tabindex="-1">色彩配置</button>
      <button type="button" class="admin-tab" id="tab-cube" role="tab" aria-selected="false" aria-controls="panel-cube" data-target="cube" tabindex="-1">魔方面板</button>
      <button type="button" class="admin-tab" id="tab-pages" role="tab" aria-selected="false" aria-controls="panel-pages" data-target="pages" tabindex="-1">页面内容</button>
    </aside>

    <main class="admin-content">
      <section id="panel-site" class="admin-panel" role="tabpanel" aria-labelledby="tab-site" aria-hidden="false">
        <h2>站点设置</h2>
        <div class="grid">
          <label>
            <span>公司名称</span>
            <input type="text" id="companyName" />
        </label>
        <label>
          <span>主页链接</span>
          <input type="text" id="homeRoute" placeholder="/" />
        </label>
        <label>
          <span>备案号</span>
          <input type="text" id="icpRecord" placeholder="示例：渝ICP备12345678号" />
        </label>
        <label>
          <span>后台提示</span>
          <textarea id="adminHint" placeholder="后台提示信息"></textarea>
        </label>
        <label>
          <span>导航配置</span>
          <div class="grid" id="navList"></div>
        </label>
        <div class="toolbar">
          <button id="btnAddNav">新增导航</button>
          <button id="btnSaveSite">保存站点配置</button>
          <span class="status" id="siteStatus"></span>
        </div>
      </div>
      </section>

      <section id="panel-theme" class="admin-panel hidden" role="tabpanel" aria-labelledby="tab-theme" aria-hidden="true">
        <h2>色彩配置</h2>
        <p class="muted theme-hint">主色调、背景容器与文字交互色统一在此维护，保存站点配置后前台即可同步生效。</p>
        <div class="theme-grid">
          <div class="theme-group">
            <h3>核心色调</h3>
          <label class="color-field">
            <span>主色 Primary</span>
            <div class="color-control" data-theme-key="palette.primary">
              <input type="color" class="color-picker" />
              <input type="text" class="color-value" placeholder="#165DFF" maxlength="7" spellcheck="false" />
            </div>
          </label>
          <label class="color-field">
            <span>强调色 Accent</span>
            <div class="color-control" data-theme-key="palette.accent">
              <input type="color" class="color-picker" />
              <input type="text" class="color-value" placeholder="#FF7D00" maxlength="7" spellcheck="false" />
            </div>
          </label>
          <label class="color-field">
            <span>中性色 Neutral</span>
            <div class="color-control" data-theme-key="palette.neutral">
              <input type="color" class="color-picker" />
              <input type="text" class="color-value" placeholder="#E6E9EF" maxlength="7" spellcheck="false" />
            </div>
          </label>
        </div>

        <div class="theme-group">
          <h3>背景与容器</h3>
          <label class="color-field">
            <span>页面背景</span>
            <div class="color-control" data-theme-key="surfaces.page">
              <input type="color" class="color-picker" />
              <input type="text" class="color-value" placeholder="#0B0C10" maxlength="7" spellcheck="false" />
            </div>
          </label>
          <label class="color-field">
            <span>导航栏背景</span>
            <div class="color-control" data-theme-key="surfaces.nav">
              <input type="color" class="color-picker" />
              <input type="text" class="color-value" placeholder="#101F40" maxlength="7" spellcheck="false" />
            </div>
          </label>
          <label class="color-field">
            <span>3D 容器底色</span>
            <div class="color-control" data-theme-key="surfaces.cube">
              <input type="color" class="color-picker" />
              <input type="text" class="color-value" placeholder="#1E40AF" maxlength="7" spellcheck="false" />
            </div>
          </label>
        </div>

        <div class="theme-group theme-group--gradients">
          <h3>渐变设置</h3>
          <p class="helper-text">为页面背景、导航栏及 3D 容器定义渐变色，可独立启用或关闭。</p>
          <div class="surface-gradient-panel">
            <div class="surface-gradient-item" data-gradient-key="page">
              <div class="surface-gradient-item__header">
                <span>页面背景</span>
                <label class="surface-gradient-toggle">
                  <input type="checkbox" data-gradient-toggle="surfaces.gradients.page.enabled" />
                  <span>启用渐变</span>
                </label>
              </div>
              <div class="surface-gradient-item__body">
                <label class="color-field">
                  <span>起始颜色</span>
                  <div class="color-control" data-theme-key="surfaces.gradients.page.from">
                    <input type="color" class="color-picker" />
                    <input type="text" class="color-value" placeholder="#165DFF" maxlength="7" spellcheck="false" />
                  </div>
                </label>
                <label class="color-field">
                  <span>结束颜色</span>
                  <div class="color-control" data-theme-key="surfaces.gradients.page.to">
                    <input type="color" class="color-picker" />
                    <input type="text" class="color-value" placeholder="#101F40" maxlength="7" spellcheck="false" />
                  </div>
                </label>
                <label class="surface-gradient-angle-field">
                  <span>角度 (0°-359°)</span>
                  <input type="number" class="surface-gradient-angle-input" data-gradient-angle="surfaces.gradients.page.angle" min="0" max="359" />
                </label>
                <p class="helper-text small">0° 表示自上向下，角度顺时针旋转。</p>
              </div>
            </div>

            <div class="surface-gradient-item" data-gradient-key="nav">
              <div class="surface-gradient-item__header">
                <span>导航栏背景</span>
                <label class="surface-gradient-toggle">
                  <input type="checkbox" data-gradient-toggle="surfaces.gradients.nav.enabled" />
                  <span>启用渐变</span>
                </label>
              </div>
              <div class="surface-gradient-item__body">
                <label class="color-field">
                  <span>起始颜色</span>
                  <div class="color-control" data-theme-key="surfaces.gradients.nav.from">
                    <input type="color" class="color-picker" />
                    <input type="text" class="color-value" placeholder="#101F40" maxlength="7" spellcheck="false" />
                  </div>
                </label>
                <label class="color-field">
                  <span>结束颜色</span>
                  <div class="color-control" data-theme-key="surfaces.gradients.nav.to">
                    <input type="color" class="color-picker" />
                    <input type="text" class="color-value" placeholder="#165DFF" maxlength="7" spellcheck="false" />
                  </div>
                </label>
                <label class="surface-gradient-angle-field">
                  <span>角度 (0°-359°)</span>
                  <input type="number" class="surface-gradient-angle-input" data-gradient-angle="surfaces.gradients.nav.angle" min="0" max="359" />
                </label>
                <p class="helper-text small">可营造顶部横向渐变光效。</p>
              </div>
            </div>

            <div class="surface-gradient-item" data-gradient-key="cube">
              <div class="surface-gradient-item__header">
                <span>3D 容器底色</span>
                <label class="surface-gradient-toggle">
                  <input type="checkbox" data-gradient-toggle="surfaces.gradients.cube.enabled" />
                  <span>启用渐变</span>
                </label>
              </div>
              <div class="surface-gradient-item__body">
                <label class="color-field">
                  <span>起始颜色</span>
                  <div class="color-control" data-theme-key="surfaces.gradients.cube.from">
                    <input type="color" class="color-picker" />
                    <input type="text" class="color-value" placeholder="#1E40AF" maxlength="7" spellcheck="false" />
                  </div>
                </label>
                <label class="color-field">
                  <span>结束颜色</span>
                  <div class="color-control" data-theme-key="surfaces.gradients.cube.to">
                    <input type="color" class="color-picker" />
                    <input type="text" class="color-value" placeholder="#0B0C10" maxlength="7" spellcheck="false" />
                  </div>
                </label>
                <label class="surface-gradient-angle-field">
                  <span>角度 (0°-359°)</span>
                  <input type="number" class="surface-gradient-angle-input" data-gradient-angle="surfaces.gradients.cube.angle" min="0" max="359" />
                </label>
                <p class="helper-text small">影响首页魔方舞台背景渐变方向。</p>
              </div>
            </div>
          </div>
        </div>

        <div class="theme-group">
          <h3>文字与交互</h3>
          <label class="color-field">
            <span>标题文字</span>
            <div class="color-control" data-theme-key="text.heading">
              <input type="color" class="color-picker" />
              <input type="text" class="color-value" placeholder="#E6E9EF" maxlength="7" spellcheck="false" />
            </div>
          </label>
          <label class="color-field">
            <span>正文文字</span>
            <div class="color-control" data-theme-key="text.body">
              <input type="color" class="color-picker" />
              <input type="text" class="color-value" placeholder="#C6CCDA" maxlength="7" spellcheck="false" />
            </div>
          </label>
          <label class="color-field">
            <span>反白文字</span>
            <div class="color-control" data-theme-key="text.inverse">
              <input type="color" class="color-picker" />
              <input type="text" class="color-value" placeholder="#0B0C10" maxlength="7" spellcheck="false" />
            </div>
          </label>
          <label class="color-field">
            <span>交互反馈</span>
            <div class="color-control" data-theme-key="text.interactive">
              <input type="color" class="color-picker" />
              <input type="text" class="color-value" placeholder="#165DFF" maxlength="7" spellcheck="false" />
            </div>
          </label>
        </div>
      </div>
      <div class="toolbar">
        <button id="btnSaveTheme">保存色彩配置</button>
        <span class="status" id="themeStatus"></span>
      </div>
      </section>

      <section id="panel-cube" class="admin-panel hidden" role="tabpanel" aria-labelledby="tab-cube" aria-hidden="true">
        <h2>魔方面板</h2>
        <p class="muted">可为每一面上传图片（推荐 1:1 比例，≥1024px），或保持渐变背景。</p>
        <div class="grid" id="faceList"></div>
        <div class="toolbar">
          <button id="btnSaveCube">保存魔方配置</button>
          <span class="status" id="cubeStatus"></span>
        </div>
      </section>

      <section id="panel-pages" class="admin-panel hidden" role="tabpanel" aria-labelledby="tab-pages" aria-hidden="true">
        <h2>页面内容</h2>
        <p class="muted">以 JSON 形式维护各业务页面内容。修改后请确保语法正确再保存。</p>
        <div class="grid" id="pagesList"></div>
      </section>
    </main>
  </div>

  <noscript>
    <style>
      .hidden {
        display: block !important;
      }
    </style>
  </noscript>

  <template id="navItemTemplate">
    <div class="card">
      <div class="inline" style="justify-content: space-between;">
        <strong class="nav-label"></strong>
        <button class="danger btn-remove-nav">删除</button>
      </div>
      <label>
        <span>唯一键（英文/拼音）</span>
        <input type="text" class="nav-key" />
      </label>
      <label>
        <span>名称</span>
        <input type="text" class="nav-text" />
      </label>
      <label>
        <span>跳转链接</span>
        <input type="text" class="nav-route" />
      </label>
      <label>
        <span>引用页面（页面键，可选）</span>
        <input type="text" class="nav-page-ref" placeholder="如 about" />
      </label>
      <label>
        <span>自定义摘要</span>
        <textarea class="nav-summary" rows="2" spellcheck="false"></textarea>
      </label>
    </div>
  </template>

  <template id="faceTemplate">
    <div class="card">
      <div class="inline" style="justify-content: space-between;">
        <strong class="face-title"></strong>
        <button class="danger btn-reset-image">清除图片</button>
      </div>
      <div class="face-preview" style="border:1px dashed var(--border);border-radius:12px;min-height:120px;display:grid;place-items:center;background:rgba(10,14,24,0.6);overflow:hidden;">
        <span class="muted">未设置图片</span>
      </div>
      <label>
        <span>标题</span>
        <input type="text" class="face-label" />
      </label>
      <label>
        <span>副标题</span>
        <input type="text" class="face-subtitle" />
      </label>
      <label>
        <span>摘要描述</span>
        <textarea class="face-description" rows="2" spellcheck="false"></textarea>
      </label>
      <label>
        <span>引用页面（页面键，可选）</span>
        <input type="text" class="face-page-ref" placeholder="如 about" />
      </label>
      <label>
        <span>自定义摘要（覆盖引用）</span>
        <textarea class="face-summary" rows="2" spellcheck="false"></textarea>
      </label>
      <label>
        <span>跳转链接</span>
        <input type="text" class="face-route" />
      </label>
      <label>
        <span>主色（渐变起点）</span>
        <input type="text" class="face-color-from" placeholder="#2B63FF" />
      </label>
      <label>
        <span>次色（渐变终点）</span>
        <input type="text" class="face-color-to" placeholder="#1232A4" />
      </label>
      <div class="inline">
        <input type="file" class="face-file" accept="image/*" />
        <button class="btn-upload-image">上传图片</button>
      </div>
      <span class="muted face-image-path"></span>
      <div class="inline" style="gap:0.8rem;">
        <label style="flex:1;">
          <span>按钮文案</span>
          <input type="text" class="face-cta-label" />
        </label>
        <label style="flex:1;">
          <span>按钮链接</span>
          <input type="text" class="face-cta-href" />
        </label>
      </div>
    </div>
  </template>

  <template id="pageEditorTemplate">
    <details class="page-editor" open>
      <summary>
        <strong class="page-title"></strong>
        <span class="status page-status"></span>
      </summary>
      <div class="page-editor-body">
        <div class="page-forms">
          <section class="page-card">
            <h3>基础信息</h3>
            <div class="field-grid">
              <label>
                <span>页面标题</span>
                <input type="text" class="field-meta-title" />
              </label>
              <label>
                <span>页面摘要</span>
                <textarea class="field-meta-summary" rows="3"></textarea>
              </label>
              <div class="inline-inputs">
                <label>
                  <span>Hero 背景图</span>
                  <input type="text" class="field-meta-hero-image" />
                </label>
                <label>
                  <span>语言标识</span>
                  <input type="text" class="field-meta-locale" />
                </label>
              </div>
              <label>
                <span>页面标签（每行一个）</span>
                <textarea class="field-meta-tags" rows="3"></textarea>
              </label>
              <div class="inline-inputs">
                <label>
                  <span>CTA 文案</span>
                  <input type="text" class="field-meta-cta-label" />
                </label>
                <label>
                  <span>CTA 链接</span>
                  <input type="text" class="field-meta-cta-href" />
                </label>
              </div>
            </div>
          </section>
          <section class="page-card">
            <h3>Hero 模块</h3>
            <div class="field-grid">
              <div class="inline-inputs">
                <label>
                  <span>标签</span>
                  <input type="text" class="field-hero-eyebrow" />
                </label>
                <label>
                  <span>徽章</span>
                  <input type="text" class="field-hero-badge" />
                </label>
              </div>
              <label>
                <span>主标题</span>
                <input type="text" class="field-hero-title" />
              </label>
              <label>
                <span>副标题</span>
                <input type="text" class="field-hero-subtitle" />
              </label>
              <label>
                <span>导语 / 摘要</span>
                <textarea class="field-hero-lead" rows="3"></textarea>
              </label>
              <label>
                <span>补充说明</span>
                <textarea class="field-hero-description" rows="3"></textarea>
              </label>
              <div class="inline-inputs">
                <label>
                  <span>CTA 文案</span>
                  <input type="text" class="field-hero-cta-label" />
                </label>
                <label>
                  <span>CTA 链接</span>
                  <input type="text" class="field-hero-cta-href" />
                </label>
              </div>
              <div class="inline-inputs">
                <label>
                  <span>媒体类型</span>
                  <select class="field-hero-media-type">
                    <option value="">无</option>
                    <option value="image">图片</option>
                    <option value="video">视频</option>
                  </select>
                </label>
                <label>
                  <span>媒体地址</span>
                  <input type="text" class="field-hero-media-src" />
                </label>
                <label>
                  <span>替换文本</span>
                  <input type="text" class="field-hero-media-alt" />
                </label>
              </div>
              <label>
                <span>亮点列表（每行一条）</span>
                <textarea class="field-hero-meta" rows="3"></textarea>
              </label>
              <label>
                <span>指标（每行格式：值|标签|描述）</span>
                <textarea class="field-hero-metrics" rows="3" placeholder="320+|年度交付项目|涵盖智驾等"></textarea>
              </label>
            </div>
          </section>
          <section class="page-card sections-card">
            <div class="sections-head">
              <h3>内容模块</h3>
              <div class="sections-actions">
                <select class="field-section-type">
                  <option value="cards">卡片组</option>
                  <option value="list">列表</option>
                  <option value="gallery">图片画廊</option>
                <option value="contact">联系方式</option>
                <option value="richtext">富文本</option>
                <option value="basic">基础文本</option>
              </select>
              <button type="button" class="btn-add-section">添加模块</button>
              <button type="button" class="btn-paste-section">粘贴模块</button>
            </div>
          </div>
            <p class="helper-text">模块顺序支持上移 / 下移，字段留空时会在前台自动隐藏。</p>
            <div class="sections-list"></div>
            <p class="section-empty">暂无模块，请先添加。</p>
          </section>
        </div>
        <details class="json-card" open>
          <summary>原始 JSON</summary>
          <label>
            <textarea class="page-json" rows="18" spellcheck="false"></textarea>
          </label>
          <div class="toolbar">
            <button type="button" class="btn-save-page">保存 JSON</button>
            <button type="button" class="btn-sync-json">从 JSON 同步到表单</button>
            <button type="button" class="btn-save-draft">保存草稿</button>
            <button type="button" class="btn-publish">发布上线</button>
            <button type="button" class="btn-preview">预览草稿</button>
          </div>
          <div class="preview-banner hidden">
            <span class="preview-message"></span>
            <code class="preview-url"></code>
            <button type="button" class="btn-copy-preview" disabled>复制预览链接</button>
          </div>
        </details>
        <div class="page-hints">
          <strong>草稿校验提示</strong>
          <ul class="hint-list"></ul>
        </div>
      </div>
    </details>
  </template>

  <script>
    const pageKeys = ['about', 'services', 'products', 'solutions', 'team', 'contact'];

    const state = {
      site: null,
      cube: null,
      pages: {},
      token: ''
    };

    const els = {
      token: document.getElementById('adminToken'),
      remember: document.getElementById('btnRemember'),
      company: document.getElementById('companyName'),
      homeRoute: document.getElementById('homeRoute'),
      icp: document.getElementById('icpRecord'),
      hint: document.getElementById('adminHint'),
      navList: document.getElementById('navList'),
      addNav: document.getElementById('btnAddNav'),
      saveSite: document.getElementById('btnSaveSite'),
      siteStatus: document.getElementById('siteStatus'),
      faceList: document.getElementById('faceList'),
      saveCube: document.getElementById('btnSaveCube'),
      cubeStatus: document.getElementById('cubeStatus'),
      pagesList: document.getElementById('pagesList'),
      tabs: Array.from(document.querySelectorAll('.admin-tab')),
      panels: Array.from(document.querySelectorAll('.admin-panel')),
      saveTheme: document.getElementById('btnSaveTheme'),
      themeStatus: document.getElementById('themeStatus'),
      gradientEnabled: document.getElementById('gradientEnabled'),
      gradientAngle: document.getElementById('gradientAngle'),
      gradientFieldWrappers: Array.from(document.querySelectorAll('[data-gradient-field]'))
    };

    // 面板状态在本地存储中的键名
    const PANEL_STORAGE_KEY = 'admin.activePanel';
    // 建立面板键与节点的映射，便于快速切换可见性
    const panelMap = new Map();
    els.panels.forEach((panel) => {
      const key = panel.id.replace(/^panel-/, '');
      if (key) {
        panelMap.set(key, panel);
      }
    });

    // 规范化面板标识，确保只有有效键值可以被激活
    function normalizePanelKey(value) {
      if (value === null || value === undefined) return null;
      const key = String(value).replace(/^#/, '').trim();
      return panelMap.has(key) ? key : null;
    }

    const defaultPanel = normalizePanelKey(els.tabs[0]?.dataset.target) || panelMap.keys().next().value || 'site';
    let activePanelKey = defaultPanel;

    // 激活指定面板：同步导航选中态、可见性、URL 与本地存储
    function activatePanel(target, options = {}) {
      const normalized = normalizePanelKey(target) || defaultPanel;
      const { updateHash = true, updateStorage = true, focusTab = false } = options;

      els.tabs.forEach((tab) => {
        const isActive = tab.dataset.target === normalized;
        tab.classList.toggle('active', isActive);
        tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
        tab.setAttribute('tabindex', isActive ? '0' : '-1');
        if (isActive && focusTab) {
          tab.focus();
        }
      });

      panelMap.forEach((panel, key) => {
        const isActive = key === normalized;
        panel.classList.toggle('hidden', !isActive);
        panel.setAttribute('aria-hidden', isActive ? 'false' : 'true');
      });

      if (updateStorage) {
        try {
          localStorage.setItem(PANEL_STORAGE_KEY, normalized);
        } catch (err) {
          console.warn('[Admin] 无法写入面板状态：', err);
        }
      }

      if (updateHash) {
        const hashValue = `#${normalized}`;
        if (window.location.hash !== hashValue) {
          const scrollY = window.scrollY;
          window.location.hash = normalized;
          window.scrollTo(0, scrollY);
        }
      }

      activePanelKey = normalized;
    }

    // 解析初始面板来源（优先 URL hash，其次 localStorage，再回退默认）
    function resolveInitialPanel() {
      const fromHash = normalizePanelKey(window.location.hash);
      if (fromHash) return fromHash;
      try {
        const stored = normalizePanelKey(localStorage.getItem(PANEL_STORAGE_KEY));
        if (stored) return stored;
      } catch (err) {
        console.warn('[Admin] 读取面板状态失败：', err);
      }
      return defaultPanel;
    }

    // 键盘辅助：根据索引聚焦目标导航项
    function focusTabByIndex(index) {
      const targetTab = els.tabs[index];
      if (targetTab) {
        targetTab.focus();
      }
    }

    // 处理键盘切换逻辑（↑/↓、空格、回车）
    function handleTabKeydown(event) {
      const { key } = event;
      const currentIndex = els.tabs.indexOf(event.currentTarget);
      if (currentIndex === -1) return;
      if (key === 'ArrowDown') {
        event.preventDefault();
        const next = (currentIndex + 1) % els.tabs.length;
        focusTabByIndex(next);
      } else if (key === 'ArrowUp') {
        event.preventDefault();
        const prev = (currentIndex - 1 + els.tabs.length) % els.tabs.length;
        focusTabByIndex(prev);
      } else if (key === 'Enter' || key === ' ') {
        event.preventDefault();
        event.currentTarget.click();
      }
    }

    // 绑定导航事件：点击切换面板并记录状态
    els.tabs.forEach((tab) => {
      const target = normalizePanelKey(tab.dataset.target);
      if (!target) return;
      tab.addEventListener('click', () => {
        activatePanel(target, { updateHash: true, updateStorage: true });
      });
      tab.addEventListener('keydown', handleTabKeydown);
    });

    // 监听 hash 变化，支持浏览器前进/后退
    window.addEventListener('hashchange', () => {
      const target = normalizePanelKey(window.location.hash) || defaultPanel;
      if (target === activePanelKey) return;
      activatePanel(target, { updateHash: false, updateStorage: true, focusTab: true });
    });

    const initialPanel = resolveInitialPanel();
    activatePanel(initialPanel, { updateHash: false, updateStorage: true });
    if (normalizePanelKey(window.location.hash) !== initialPanel) {
      if (typeof history.replaceState === 'function') {
        history.replaceState(null, '', `#${initialPanel}`);
      } else {
        const scrollY = window.scrollY;
        window.location.hash = initialPanel;
        window.scrollTo(0, scrollY);
      }
    }

    const api = {
      site: '/api/content/site',
      cube: '/api/content/cube',
      upload: '/api/uploads'
    };

    const schemaSources = {
      page: '/schemas/page.schema.json'
    };
    let pageSchema = null;

    async function ensurePageSchema() {
      if (pageSchema) return pageSchema;
      const res = await fetch(schemaSources.page, { cache: 'no-store' });
      if (!res.ok) {
        throw new Error('无法加载页面结构 Schema');
      }
      pageSchema = await res.json();
      return pageSchema;
    }

    function schemaResolveRef(schema, ref) {
      if (!ref || typeof ref !== 'string' || !ref.startsWith('#/')) return null;
      const parts = ref
        .slice(2)
        .split('/')
        .map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~'));
      let current = schema;
      for (const part of parts) {
        if (!current || typeof current !== 'object') return null;
        current = current[part];
      }
      return current || null;
    }

    function applySchemaDefaults(schema, value, root = schema) {
      if (!schema) return value;
      if (schema.$ref) {
        const resolved = schemaResolveRef(root, schema.$ref);
        return applySchemaDefaults(resolved, value, root);
      }
      if (schema.anyOf) {
        if (value === undefined && Object.prototype.hasOwnProperty.call(schema, 'default')) {
          return deepClone(schema.default);
        }
        if (value === null) {
          const nullVariant = schema.anyOf.find((option) => option && option.type === 'null');
          if (nullVariant) return null;
        }
        const objectVariant = schema.anyOf.find((option) => option && option.type && option.type !== 'null');
        if (objectVariant) {
          return applySchemaDefaults(objectVariant, value, root);
        }
        return value;
      }

      const type = schema.type;
      let current = value;
      if (current === undefined || current === null) {
        if (Object.prototype.hasOwnProperty.call(schema, 'default')) {
          current = deepClone(schema.default);
        } else if (type === 'object') {
          current = {};
        } else if (type === 'array') {
          current = [];
        }
      }

      if (type === 'object') {
        const result = current && typeof current === 'object' && !Array.isArray(current) ? { ...current } : {};
        const props = schema.properties || {};
        Object.keys(props).forEach((key) => {
          result[key] = applySchemaDefaults(props[key], result[key], root);
          if (result[key] === undefined) {
            delete result[key];
          }
        });
        if (schema.additionalProperties === false) {
          Object.keys(result).forEach((key) => {
            if (!props[key]) {
              delete result[key];
            }
          });
        }
        return result;
      }

      if (type === 'array') {
        const itemsSchema = schema.items;
        if (!Array.isArray(current)) {
          current = [];
        }
        if (itemsSchema) {
          return current.map((entry) => applySchemaDefaults(itemsSchema, entry, root));
        }
        return current;
      }

      return current;
    }

    function validateSchemaValue(schema, value, path, errors, root = schema) {
      if (!schema) return errors;
      if (schema.$ref) {
        const resolved = schemaResolveRef(root, schema.$ref);
        return validateSchemaValue(resolved, value, path, errors, root);
      }
      if (schema.anyOf) {
        const outcomes = schema.anyOf.map((option) => {
          const optionErrors = [];
          validateSchemaValue(option, value, path, optionErrors, root);
          return optionErrors;
        });
        if (outcomes.some((entry) => entry.length === 0)) {
          return errors;
        }
        const firstIssue = outcomes.find((entry) => entry.length)?.[0];
        errors.push(firstIssue || `${path} 不符合预期结构`);
        return errors;
      }

      const type = schema.type;
      if (type === 'object') {
        if (!value || typeof value !== 'object' || Array.isArray(value)) {
          errors.push(`${path} 需为对象`);
          return errors;
        }
        const props = schema.properties || {};
        if (schema.additionalProperties === false) {
          Object.keys(value).forEach((key) => {
            if (!props[key]) {
              errors.push(`${path}.${key} 非允许字段`);
            }
          });
        }
        if (Array.isArray(schema.required)) {
          schema.required.forEach((key) => {
            if (value[key] === undefined || value[key] === null) {
              errors.push(`${path}.${key} 为必填字段`);
            }
          });
        }
        Object.keys(props).forEach((key) => {
          if (value[key] !== undefined) {
            validateSchemaValue(props[key], value[key], `${path}.${key}`, errors, root);
          } else if (Array.isArray(schema.required) && schema.required.includes(key)) {
            errors.push(`${path}.${key} 为必填字段`);
          }
        });
        return errors;
      }

      if (type === 'array') {
        if (!Array.isArray(value)) {
          errors.push(`${path} 需为数组`);
          return errors;
        }
        if (typeof schema.minItems === 'number' && value.length < schema.minItems) {
          errors.push(`${path} 至少包含 ${schema.minItems} 个条目`);
        }
        if (schema.items) {
          value.forEach((entry, index) => {
            validateSchemaValue(schema.items, entry, `${path}[${index}]`, errors, root);
          });
        }
        return errors;
      }

      if (type === 'string') {
        if (typeof value !== 'string') {
          errors.push(`${path} 需为字符串`);
          return errors;
        }
        if (typeof schema.minLength === 'number' && value.trim().length < schema.minLength) {
          errors.push(`${path} 至少包含 ${schema.minLength} 个字符`);
        }
        if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {
          errors.push(`${path} 取值须为 ${schema.enum.join(', ')}`);
        }
        return errors;
      }

      if (type === 'boolean') {
        if (typeof value !== 'boolean') {
          errors.push(`${path} 需为布尔值`);
        }
        return errors;
      }

      if (type === 'number' || type === 'integer') {
        if (typeof value !== 'number' || Number.isNaN(value)) {
          errors.push(`${path} 需为数值`);
        }
        return errors;
      }

      if (type === 'null') {
        if (value !== null) {
          errors.push(`${path} 需为 null`);
        }
        return errors;
      }

      return errors;
    }

    function enforcePageSchema(raw) {
      if (!pageSchema) {
        return { value: raw, errors: [] };
      }
      const hydrated = applySchemaDefaults(pageSchema, raw, pageSchema);
      const errors = [];
      validateSchemaValue(pageSchema, hydrated, '$', errors, pageSchema);
      return { value: hydrated, errors };
    }

    const sectionTypeLabels = {
      cards: '卡片组',
      list: '列表',
      timeline: '时间轴',
      stats: '数据指标',
      gallery: '图片画廊',
      contact: '联系方式',
      richtext: '富文本',
      basic: '基础文本'
    };

    const sectionTypeOrder = Object.keys(sectionTypeLabels);

    function tidyString(value, fallback = '') {
      if (value === null || value === undefined) return fallback;
      if (typeof value === 'string') return value.trim();
      if (typeof value === 'number' || typeof value === 'boolean') return String(value).trim();
      return fallback;
    }

    function parseLines(text) {
      return String(text || '')
        .split(/\r?\n/)
        .map((line) => line.trim())
        .filter((line) => !!line);
    }

    function joinLines(list) {
      return Array.isArray(list) && list.length ? list.join('\n') : '';
    }

    function parseMetrics(text) {
      return parseLines(text)
        .map((line) => {
          const parts = line.split('|').map((part) => part.trim());
          const [value = '', label = '', description = ''] = parts;
          return { value, label, description };
        })
        .filter((item) => item.value || item.label || item.description);
    }

    function formatMetrics(metrics) {
      if (!Array.isArray(metrics) || !metrics.length) return '';
      return metrics
        .map((item) => {
          const parts = [tidyString(item.value), tidyString(item.label), tidyString(item.description)];
          while (parts.length && !parts[parts.length - 1]) {
            parts.pop();
          }
          return parts.join('|');
        })
        .join('\n');
    }

    function formatTags(tags) {
      return Array.isArray(tags) && tags.length ? tags.join('\n') : '';
    }

    function formatSchemaMessage(message) {
      if (!message) return '';
      let text = message;
      const replacements = [
        { pattern: /\$\.meta\.title/g, replacement: '基础信息 · 页面标题' },
        { pattern: /\$\.meta\.summary/g, replacement: '基础信息 · 页面摘要' },
        { pattern: /\$\.draft\.hero\.title/g, replacement: 'Hero 模块 · 主标题' },
        { pattern: /\$\.draft\.hero\.lead/g, replacement: 'Hero 模块 · 导语' },
        { pattern: /\$\.draft\.hero/g, replacement: 'Hero 模块' },
        { pattern: /\$\.draft\.sections/g, replacement: '内容模块' },
        { pattern: /\$\.draft/g, replacement: '草稿内容' },
        { pattern: /\$\.published/g, replacement: '已发布内容' }
      ];
      replacements.forEach(({ pattern, replacement }) => {
        text = text.replace(pattern, replacement);
      });
      text = text.replace(/\$\./g, '');
      text = text.replace(/非允许字段/g, '字段不被支持');
      text = text.replace(/需为对象/g, '需为有效的对象');
      text = text.replace(/需为数组/g, '需为有效的数组');
      text = text.replace(/需为字符串/g, '需为文本');
      text = text.replace(/至少包含 (\d+) 个字符/g, '至少填写 $1 个字符');
      text = text.replace(/取值须为 ([^$]+)/g, '取值需为 $1');
      return text;
    }

    function moveArrayItem(list, fromIndex, toIndex) {
      if (!Array.isArray(list)) return false;
      if (
        fromIndex === toIndex ||
        fromIndex < 0 ||
        toIndex < 0 ||
        fromIndex >= list.length ||
        toIndex >= list.length
      ) {
        return false;
      }
      const [item] = list.splice(fromIndex, 1);
      list.splice(toIndex, 0, item);
      return true;
    }

    function setupDragSort(container, options = {}) {
      if (!container) return;
      const { itemSelector, handleSelector, onMove, hoverClass = 'drag-over' } = options;
      if (!itemSelector || typeof onMove !== 'function') return;
      const state =
        container.__dragSortState ||
        {
          dragIndex: -1,
          bound: false
        };
      state.itemSelector = itemSelector;
      state.handleSelector = handleSelector;
      state.onMove = onMove;
      state.hoverClass = hoverClass;
      container.__dragSortState = state;

      const getItems = () => Array.from(container.querySelectorAll(state.itemSelector));
      const getIndex = (target) => getItems().indexOf(target);

      getItems().forEach((item) => {
        const handle = state.handleSelector ? item.querySelector(state.handleSelector) : item;
        if (!handle || handle.dataset.dragBound === 'true') return;
        handle.dataset.dragBound = 'true';
        handle.setAttribute('draggable', 'true');
        handle.addEventListener('dragstart', (event) => {
          state.dragIndex = getIndex(item);
          if (state.dragIndex === -1) return;
          event.dataTransfer.effectAllowed = 'move';
          event.dataTransfer.setData('text/plain', String(state.dragIndex));
          item.classList.add('is-dragging');
          event.stopPropagation();
        });
        handle.addEventListener('dragend', () => {
          state.dragIndex = -1;
          item.classList.remove('is-dragging');
          getItems().forEach((node) => node.classList.remove(state.hoverClass));
        });
      });

      if (!state.bound) {
        container.addEventListener('dragover', (event) => {
          if (state.dragIndex === -1) return;
          event.preventDefault();
          event.dataTransfer.dropEffect = 'move';
          const targetItem = event.target.closest(state.itemSelector);
          getItems().forEach((node) => node.classList.remove(state.hoverClass));
          if (targetItem && container.contains(targetItem)) {
            targetItem.classList.add(state.hoverClass);
          }
        });

        container.addEventListener('dragleave', (event) => {
          const targetItem = event.target.closest(state.itemSelector);
          if (targetItem && container.contains(targetItem)) {
            targetItem.classList.remove(state.hoverClass);
          }
        });

        container.addEventListener('drop', (event) => {
          if (state.dragIndex === -1) return;
          event.preventDefault();
          const targetItem = event.target.closest(state.itemSelector);
          getItems().forEach((node) => node.classList.remove(state.hoverClass));
          if (!targetItem || !container.contains(targetItem)) {
            state.dragIndex = -1;
            return;
          }
          const targetIndex = getIndex(targetItem);
          if (targetIndex === -1 || targetIndex === state.dragIndex) {
            state.dragIndex = -1;
            return;
          }
          state.onMove(state.dragIndex, targetIndex);
          state.dragIndex = -1;
        });

        state.bound = true;
      }
    }

    const clipboardStore = {
      section: '',
      subitems: ''
    };

    async function writeClipboardPayload(kind, data) {
      const record = { kind, version: 1, data };
      const text = JSON.stringify(record);
      clipboardStore[kind] = text;
      try {
        if (navigator.clipboard?.writeText) {
          await navigator.clipboard.writeText(text);
          return true;
        }
      } catch (err) {
        console.warn('[Clipboard] writeText failed', err);
      }
      try {
        const helper = document.createElement('textarea');
        helper.value = text;
        helper.setAttribute('readonly', '');
        helper.style.position = 'fixed';
        helper.style.opacity = '0';
        document.body.appendChild(helper);
        helper.focus();
        helper.select();
        const succeeded = document.execCommand('copy');
        document.body.removeChild(helper);
        if (succeeded) {
          return true;
        }
      } catch (err) {
        console.warn('[Clipboard] execCommand copy failed', err);
      }
      window.prompt('复制以下内容后手动粘贴：', text);
      return false;
    }

    function parseClipboardPayload(text) {
      if (!text) return null;
      try {
        const payload = JSON.parse(text);
        return payload && typeof payload === 'object' ? payload : null;
      } catch (err) {
        return null;
      }
    }

    async function readClipboardPayload(kind) {
      let text = null;
      if (navigator.clipboard?.readText) {
        try {
          text = await navigator.clipboard.readText();
        } catch (err) {
          console.warn('[Clipboard] readText failed', err);
        }
      }
      if (!text && clipboardStore[kind]) {
        text = clipboardStore[kind];
      }
      if (!text) {
        text = window.prompt('请粘贴之前复制的内容：') || '';
      }
      if (!text) return null;
      clipboardStore[kind] = text;
      const payload = parseClipboardPayload(text);
      if (!payload || payload.kind !== kind) {
        return null;
      }
      if (payload.version !== 1) {
        return null;
      }
      return payload;
    }

    function getPreviewExpiryInfo(expiresAt) {
      if (!expiresAt) return null;
      const date = typeof expiresAt === 'number' ? new Date(expiresAt) : new Date(String(expiresAt));
      if (Number.isNaN(date.getTime())) return null;
      const diffMs = date.getTime() - Date.now();
      let relative = '';
      if (diffMs <= 0) {
        relative = '已过期';
      } else {
        const totalSeconds = Math.round(diffMs / 1000);
        const minutes = Math.floor(totalSeconds / 60);
        const seconds = totalSeconds % 60;
        if (minutes > 0) {
          relative = seconds ? `${minutes} 分 ${seconds} 秒后` : `${minutes} 分钟后`;
        } else {
          relative = '不到 1 分钟后';
        }
      }
      return {
        relative,
        absolute: date.toLocaleString(),
        expiresAt: date
      };
    }

    function deepClone(value) {
      if (value === null || value === undefined) return value;
      try {
        return JSON.parse(JSON.stringify(value));
      } catch (err) {
        return value;
      }
    }

    function ensureArray(value) {
      return Array.isArray(value) ? value : [];
    }

    function createCard() {
      return {
        title: '',
        subtitle: '',
        description: '',
        badge: '',
        media: null,
        actions: []
      };
    }

    function createListItem() {
      return {
        title: '',
        description: ''
      };
    }

    function createTimelineItem() {
      return {
        year: '',
        title: '',
        description: ''
      };
    }

    function createStat() {
      return {
        value: '',
        label: '',
        description: ''
      };
    }

    function createContactItem() {
      return {
        label: '',
        value: ''
      };
    }

    function normalizeCardEntry(entry) {
      const card = entry && typeof entry === 'object' ? entry : {};
      const normalized = {
        title: tidyString(card.title),
        subtitle: tidyString(card.subtitle),
        description: tidyString(card.description),
        badge: tidyString(card.badge),
        media: null,
        actions: []
      };
      const media = card.media && typeof card.media === 'object' ? card.media : null;
      if (media) {
        const src = tidyString(media.src);
        if (src) {
          normalized.media = {
            type: tidyString(media.type) || 'image',
            src,
            alt: tidyString(media.alt),
            caption: tidyString(media.caption),
            thumbnail: tidyString(media.thumbnail),
            ratio: tidyString(media.ratio)
          };
        }
      }
      normalized.actions = ensureArray(card.actions)
        .map((action) => {
          if (!action || typeof action !== 'object') return null;
          const label = tidyString(action.label);
          const href = tidyString(action.href);
          if (!label && !href) return null;
          return {
            label,
            href,
            target: action.target === '_blank' || /^https?:\/\//i.test(href) ? '_blank' : '_self'
          };
        })
        .filter(Boolean);
      return normalized;
    }

    function normalizeListEntry(entry) {
      const item = entry && typeof entry === 'object' ? entry : {};
      return {
        title: tidyString(item.title),
        description: tidyString(item.description)
      };
    }

    function normalizeTimelineEntry(entry) {
      const item = entry && typeof entry === 'object' ? entry : {};
      return {
        year: tidyString(item.year),
        title: tidyString(item.title),
        description: tidyString(item.description)
      };
    }

    function normalizeStatEntry(entry) {
      const item = entry && typeof entry === 'object' ? entry : {};
      return {
        value: tidyString(item.value),
        label: tidyString(item.label),
        description: tidyString(item.description)
      };
    }

    function normalizeContactEntry(entry) {
      const item = entry && typeof entry === 'object' ? entry : {};
      return {
        label: tidyString(item.label),
        value: tidyString(item.value)
      };
    }

    function normalizeGalleryItem(entry) {
      const item = entry && typeof entry === 'object' ? entry : {};
      const src = tidyString(item.src);
      return {
        src,
        alt: tidyString(item.alt),
        caption: tidyString(item.caption)
      };
    }

    function createGalleryItem() {
      return {
        src: '',
        alt: '',
        caption: ''
      };
    }

    function createSection(type = 'cards') {
      const base = {
        id: '',
        type,
        title: '',
        subtitle: '',
        description: '',
        summary: '',
        layout: '',
        tint: false,
        cta: null
      };
      switch (type) {
        case 'cards':
          base.cards = [];
          break;
        case 'list':
        case 'timeline':
        case 'contact':
        case 'gallery':
          base.items = [];
          break;
        case 'stats':
          base.stats = [];
          break;
        case 'richtext':
        case 'basic':
          base.content = '';
          break;
        default:
          base.items = [];
          break;
      }
      return base;
    }

    function normalizeSection(section) {
      const source = section && typeof section === 'object' ? section : {};
      const type = tidyString(source.type) || 'cards';
      const normalized = Object.assign(createSection(type), source, {
        type,
        id: tidyString(source.id),
        title: tidyString(source.title),
        subtitle: tidyString(source.subtitle),
        description: tidyString(source.description),
        summary: tidyString(source.summary),
        layout: tidyString(source.layout),
        tint: !!source.tint
      });
      if (source.cta && typeof source.cta === 'object') {
        const label = tidyString(source.cta.label);
        const href = tidyString(source.cta.href);
        normalized.cta =
          label || href
            ? { label, href, target: source.cta.target === '_blank' || /^https?:\/\//i.test(href) ? '_blank' : '_self' }
            : null;
      } else {
        normalized.cta = null;
      }
      switch (type) {
        case 'cards':
          normalized.cards = ensureArray(source.cards).map(normalizeCardEntry);
          break;
        case 'list':
          normalized.items = ensureArray(source.items).map(normalizeListEntry);
          break;
        case 'timeline':
          normalized.items = ensureArray(source.items).map(normalizeTimelineEntry);
          break;
        case 'contact':
          normalized.items = ensureArray(source.items).map(normalizeContactEntry);
          break;
        case 'gallery':
          normalized.items = ensureArray(source.items).map(normalizeGalleryItem);
          break;
        case 'stats':
          normalized.stats = ensureArray(source.stats).map(normalizeStatEntry);
          break;
        case 'richtext':
        case 'basic':
          normalized.content = tidyString(source.content);
          break;
        default:
          normalized.items = ensureArray(source.items).map(normalizeListEntry);
          break;
      }
      if (type !== 'cards') {
        delete normalized.cards;
      }
      if (!['list', 'timeline', 'contact', 'gallery'].includes(type)) {
        delete normalized.items;
      }
      if (type !== 'stats') {
        delete normalized.stats;
      }
      if (!['richtext', 'basic'].includes(type)) {
        delete normalized.content;
      }
      return normalized;
    }

    function preparePageDocument(raw) {
      const source = raw && typeof raw === 'object' ? raw : {};
      const { value, errors } = enforcePageSchema(source);
      const doc = value && typeof value === 'object' ? value : {};
      Object.defineProperty(doc, '__schemaErrors', {
        value: errors,
        enumerable: false,
        configurable: true,
        writable: true
      });
      doc.status = tidyString(doc.status) || 'draft';
      doc.meta = Object.assign(
        {
          title: '',
          summary: '',
          heroImage: '',
          gallery: [],
          tags: [],
          locale: 'zh-CN',
          cta: null
        },
        doc.meta || {}
      );
      doc.meta.tags = ensureArray(doc.meta.tags)
        .map((tag) => tidyString(tag))
        .filter(Boolean);
      doc.meta.gallery = ensureArray(doc.meta.gallery).map(normalizeGalleryItem);
      if (doc.meta.cta && typeof doc.meta.cta === 'object') {
        const label = tidyString(doc.meta.cta.label);
        const href = tidyString(doc.meta.cta.href);
        doc.meta.cta =
          label || href
            ? { label, href, target: doc.meta.cta.target === '_blank' || /^https?:\/\//i.test(href) ? '_blank' : '_self' }
            : null;
      } else {
        doc.meta.cta = null;
      }
      let draft = doc.draft && typeof doc.draft === 'object' ? doc.draft : null;
      if (!draft) {
        draft = doc.published && typeof doc.published === 'object' ? deepClone(doc.published) : {};
      }
      doc.draft = draft;
      doc.draft.hero = Object.assign(
        {
          eyebrow: '',
          title: '',
          subtitle: '',
          lead: '',
          description: '',
          summary: '',
          badge: '',
          meta: [],
          actions: [],
          cta: null,
          media: null,
          metrics: []
        },
        doc.draft.hero || {}
      );
      doc.draft.hero.meta = ensureArray(doc.draft.hero.meta)
        .map((entry) => tidyString(entry))
        .filter(Boolean);
      doc.draft.hero.metrics = ensureArray(doc.draft.hero.metrics).map(normalizeStatEntry);
      if (doc.draft.hero.cta && typeof doc.draft.hero.cta === 'object') {
        const label = tidyString(doc.draft.hero.cta.label);
        const href = tidyString(doc.draft.hero.cta.href);
        doc.draft.hero.cta =
          label || href
            ? {
                label,
                href,
                target: doc.draft.hero.cta.target === '_blank' || /^https?:\/\//i.test(href) ? '_blank' : '_self'
              }
            : null;
      } else {
        doc.draft.hero.cta = null;
      }
      const media = doc.draft.hero.media;
      if (media && typeof media === 'object') {
        const src = tidyString(media.src);
        if (src) {
          doc.draft.hero.media = {
            type: tidyString(media.type) || 'image',
            src,
            alt: tidyString(media.alt),
            caption: tidyString(media.caption),
            thumbnail: tidyString(media.thumbnail),
            ratio: tidyString(media.ratio)
          };
        } else {
          doc.draft.hero.media = null;
        }
      } else {
        doc.draft.hero.media = null;
      }
      doc.draft.sections = ensureArray(doc.draft.sections).map((section) => normalizeSection(section));
      doc.published = doc.published && typeof doc.published === 'object' ? doc.published : null;
      return doc;
    }

    function validatePageDocument(doc) {
      const warnings = [];
      if (!doc || typeof doc !== 'object') {
        return ['页面数据缺失，无法保存。'];
      }
      if (!tidyString(doc.meta && doc.meta.title)) {
        warnings.push('基础信息缺少页面标题。');
      }
      if (!tidyString(doc.meta && doc.meta.summary)) {
        warnings.push('基础信息缺少页面摘要。');
      }
      const hero = doc.draft?.hero || {};
      if (!tidyString(hero.title)) {
        warnings.push('Hero 模块缺少主标题。');
      }
      if (!tidyString(hero.lead)) {
        warnings.push('Hero 模块建议补充导语内容。');
      }
      const metrics = ensureArray(hero.metrics);
      if (
        metrics.some((metric) => {
          const value = tidyString(metric && metric.value);
          const label = tidyString(metric && metric.label);
          return (value && !label) || (!value && label);
        })
      ) {
        warnings.push('Hero 指标存在未成对的数值或标签，请补全后继续。');
      }
      const sections = Array.isArray(doc.draft?.sections) ? doc.draft.sections : [];
      if (!sections.length) {
        warnings.push('尚未添加任何内容模块。');
      }
      sections.forEach((entry, index) => {
        const section = normalizeSection(entry);
        const type = section.type || 'basic';
        const name = tidyString(section.title) || `模块 ${index + 1}`;
        switch (type) {
          case 'cards':
            if (!Array.isArray(section.cards) || !section.cards.length) {
              warnings.push(`${name} 缺少卡片条目。`);
            }
            break;
          case 'list':
          case 'contact':
            if (!Array.isArray(section.items) || !section.items.length) {
              warnings.push(`${name} 缺少列表条目。`);
            }
            break;
          case 'timeline':
            if (!Array.isArray(section.items) || !section.items.length) {
              warnings.push(`${name} 缺少时间节点。`);
            } else if (section.items.some((item) => !tidyString(item.year) && !tidyString(item.title))) {
              warnings.push(`${name} 存在未填写年份或标题的时间节点。`);
            }
            break;
          case 'stats':
            if (!Array.isArray(section.stats) || !section.stats.length) {
              warnings.push(`${name} 缺少指标数据。`);
            } else if (section.stats.some((item) => !tidyString(item.value) || !tidyString(item.label))) {
              warnings.push(`${name} 存在未填写数值或标签的指标。`);
            }
            break;
          case 'gallery':
            if (!Array.isArray(section.items) || !section.items.length) {
              warnings.push(`${name} 缺少图片资源。`);
            }
            break;
          case 'richtext':
          case 'basic':
            if (!tidyString(section.content)) {
              warnings.push(`${name} 富文本内容为空。`);
            }
            break;
          default:
            break;
        }
      });
      return warnings;
    }

    function initPageForm(container, key, textarea, statusEl) {
      let doc = preparePageDocument(state.pages[key]);
      state.pages[key] = doc;
      let meta = doc.meta;
      let draft = doc.draft;
      let hero = draft.hero;
      let sections = draft.sections;

      const metaTitle = container.querySelector('.field-meta-title');
      const metaSummary = container.querySelector('.field-meta-summary');
      const metaHeroImage = container.querySelector('.field-meta-hero-image');
      const metaLocale = container.querySelector('.field-meta-locale');
      const metaTags = container.querySelector('.field-meta-tags');
      const metaCtaLabel = container.querySelector('.field-meta-cta-label');
      const metaCtaHref = container.querySelector('.field-meta-cta-href');

      const heroEyebrow = container.querySelector('.field-hero-eyebrow');
      const heroBadge = container.querySelector('.field-hero-badge');
      const heroTitle = container.querySelector('.field-hero-title');
      const heroSubtitle = container.querySelector('.field-hero-subtitle');
      const heroLead = container.querySelector('.field-hero-lead');
      const heroDescription = container.querySelector('.field-hero-description');
      const heroCtaLabel = container.querySelector('.field-hero-cta-label');
      const heroCtaHref = container.querySelector('.field-hero-cta-href');
      const heroMediaType = container.querySelector('.field-hero-media-type');
      const heroMediaSrc = container.querySelector('.field-hero-media-src');
      const heroMediaAlt = container.querySelector('.field-hero-media-alt');
      const heroMeta = container.querySelector('.field-hero-meta');
      const heroMetrics = container.querySelector('.field-hero-metrics');

      const sectionsList = container.querySelector('.sections-list');
      const sectionEmpty = container.querySelector('.section-empty');
      const sectionTypeSelect = container.querySelector('.field-section-type');
      const addSectionBtn = container.querySelector('.btn-add-section');
      const pasteSectionBtn = container.querySelector('.btn-paste-section');
      const hintList = container.querySelector('.hint-list');

      if (sectionTypeSelect) {
        const currentValue = sectionTypeSelect.value;
        sectionTypeSelect.innerHTML = '';
        sectionTypeOrder.forEach((typeKey) => {
          const option = document.createElement('option');
          option.value = typeKey;
          option.textContent = sectionTypeLabels[typeKey];
          sectionTypeSelect.appendChild(option);
        });
        const defaultValue = sectionTypeOrder.includes(currentValue) ? currentValue : sectionTypeOrder[0];
        sectionTypeSelect.value = defaultValue;
      }

      function syncJson() {
        const schemaSnapshot = enforcePageSchema(doc);
        if (schemaSnapshot && schemaSnapshot.errors) {
          Object.defineProperty(doc, '__schemaErrors', {
            value: schemaSnapshot.errors,
            enumerable: false,
            configurable: true,
            writable: true
          });
        }
        const payload = schemaSnapshot?.value || doc;
        textarea.value = JSON.stringify(payload, null, 2);
      }

      function renderHints() {
        const schemaSnapshot = enforcePageSchema(doc);
        if (schemaSnapshot && schemaSnapshot.errors) {
          Object.defineProperty(doc, '__schemaErrors', {
            value: schemaSnapshot.errors,
            enumerable: false,
            configurable: true,
            writable: true
          });
        }
        const schemaWarnings = schemaSnapshot?.errors || [];
        const logicalWarnings = validatePageDocument(doc);
        const warnings = Array.from(new Set([...schemaWarnings, ...logicalWarnings]));
        hintList.innerHTML = '';
        if (!warnings.length) {
          const li = document.createElement('li');
          li.textContent = '已通过基础检查，可直接保存或发布。';
          hintList.appendChild(li);
        } else {
          warnings.forEach((message) => {
            const li = document.createElement('li');
            li.textContent = formatSchemaMessage(message);
            hintList.appendChild(li);
          });
        }
        return warnings;
      }

      function populateMeta() {
        metaTitle.value = meta.title || '';
        metaSummary.value = meta.summary || '';
        metaHeroImage.value = tidyString(meta.heroImage);
        metaLocale.value = tidyString(meta.locale || 'zh-CN');
        metaTags.value = formatTags(meta.tags);
        metaCtaLabel.value = meta.cta?.label || '';
        metaCtaHref.value = meta.cta?.href || '';
      }

      function populateHero() {
        heroEyebrow.value = hero.eyebrow || '';
        heroBadge.value = hero.badge || '';
        heroTitle.value = hero.title || '';
        heroSubtitle.value = hero.subtitle || '';
        heroLead.value = hero.lead || '';
        heroDescription.value = hero.description || '';
        heroCtaLabel.value = hero.cta?.label || '';
        heroCtaHref.value = hero.cta?.href || '';
        heroMeta.value = joinLines(hero.meta);
        heroMetrics.value = formatMetrics(hero.metrics);
        const media = hero.media && typeof hero.media === 'object' ? hero.media : null;
        heroMediaType.value = media?.src ? media.type || 'image' : '';
        heroMediaSrc.value = media?.src || '';
        heroMediaAlt.value = media?.alt || '';
      }

      function updateMetaCta() {
        const label = tidyString(metaCtaLabel.value);
        const href = tidyString(metaCtaHref.value);
        meta.cta = label || href ? { label, href, target: /^https?:\/\//i.test(href) ? '_blank' : '_self' } : null;
      }

      function updateHeroCta() {
        const label = tidyString(heroCtaLabel.value);
        const href = tidyString(heroCtaHref.value);
        hero.cta = label || href ? { label, href, target: /^https?:\/\//i.test(href) ? '_blank' : '_self' } : null;
      }

      function updateHeroMedia() {
        const type = tidyString(heroMediaType.value) || 'image';
        const src = tidyString(heroMediaSrc.value);
        const alt = tidyString(heroMediaAlt.value);
        if (src) {
          hero.media = Object.assign({}, hero.media && typeof hero.media === 'object' ? hero.media : {}, { type, src, alt });
        } else {
          hero.media = null;
        }
      }

      let lastOpenIndex = 0;

      const flashStatus = (message) => {
        if (!statusEl) return;
        statusEl.textContent = message;
        setTimeout(() => {
          if (statusEl.textContent === message) {
            statusEl.textContent = '';
          }
        }, 2200);
      };

      function transformSection(section, type) {
        if (section.type === type) return;
        const preserved = {
          id: section.id,
          title: section.title,
          subtitle: section.subtitle,
          description: section.description,
          summary: section.summary,
          layout: section.layout,
          tint: section.tint,
          cta: section.cta
        };
        const next = createSection(type);
        Object.assign(section, next, preserved);
      }

      function moveSectionTo(fromIndex, toIndex) {
        if (
          fromIndex === toIndex ||
          fromIndex < 0 ||
          toIndex < 0 ||
          fromIndex >= sections.length ||
          toIndex >= sections.length
        ) {
          return;
        }
        const [item] = sections.splice(fromIndex, 1);
        sections.splice(toIndex, 0, item);
        lastOpenIndex = toIndex;
        syncJson();
        renderSections(toIndex);
        renderHints();
      }

      function moveSection(index, delta) {
        const target = index + delta;
        moveSectionTo(index, target);
      }

      function removeSection(index) {
        sections.splice(index, 1);
        lastOpenIndex = Math.max(0, index - 1);
        syncJson();
        renderSections(lastOpenIndex);
        renderHints();
      }

      async function copySectionAt(index) {
        const original = sections[index];
        if (!original) {
          alert('未找到可复制的模块');
          return;
        }
        const payload = deepClone(original);
        const success = await writeClipboardPayload('section', payload);
        if (success) {
          flashStatus('模块已复制');
        }
      }

      async function pasteSectionAt(index) {
        const record = await readClipboardPayload('section');
        const incoming = record?.data;
        if (!incoming || typeof incoming !== 'object') {
          alert('剪贴板中没有有效的模块数据');
          return;
        }
        const cloned = normalizeSection(deepClone(incoming));
        const baseId = tidyString(cloned.id);
        cloned.id = baseId ? baseId + '-copy' : (cloned.type || 'section') + '-' + (sections.length + 1);
        if (tidyString(cloned.title)) {
          cloned.title = tidyString(cloned.title) + '（复制）';
        }
        const position = Math.max(0, Math.min(index, sections.length));
        sections.splice(position, 0, cloned);
        lastOpenIndex = position;
        syncJson();
        renderSections(position);
        renderHints();
        flashStatus('已粘贴模块');
      }

      async function copySubitems(sectionType, collection, emptyMessage, successMessage) {
        const list = ensureArray(collection);
        if (!list.length) {
          alert(emptyMessage);
          return;
        }
        const payload = { type: sectionType, items: deepClone(list) };
        const success = await writeClipboardPayload('subitems', payload);
        if (success) {
          flashStatus(successMessage);
        }
      }

      async function pasteSubitems(sectionType, targetCollection, normalizer, sectionIndex, successMessage) {
        const record = await readClipboardPayload('subitems');
        const data = record?.data;
        if (!data || typeof data !== 'object') {
          alert('剪贴板中没有可用的子项数据');
          return;
        }
        if (data.type && data.type !== sectionType) {
          alert('剪贴板子项类型与当前模块不匹配');
          return;
        }
        const items = ensureArray(data.items);
        if (!items.length) {
          alert('剪贴板子项为空');
          return;
        }
        items.forEach((entry) => {
          targetCollection.push(normalizer(deepClone(entry)));
        });
        syncJson();
        renderSections(sectionIndex);
        renderHints();
        flashStatus(successMessage);
      }

      function buildSectionSpecificFields(section, sectionIndex) {
        const wrapper = document.createElement('div');
        wrapper.className = 'section-sublist';

        switch (section.type) {
          case 'cards': {
            section.cards = ensureArray(section.cards);
            if (!section.cards.length) {
              const note = document.createElement('p');
              note.className = 'helper-text';
              note.textContent = '尚未添加卡片，点击下方按钮新增。';
              wrapper.appendChild(note);
            }
            section.cards.forEach((card, cardIndex) => {
              const item = document.createElement('div');
              item.className = 'subitem';
              const header = document.createElement('header');
              const dragHandle = document.createElement('span');
              dragHandle.className = 'drag-handle';
              dragHandle.textContent = '拖动';
              dragHandle.title = '拖拽排序';
              const title = document.createElement('span');
              title.className = 'subitem-title';
              const updateTitle = () => {
                title.textContent = `卡片 ${cardIndex + 1}${card.title ? ` · ${card.title}` : ''}`;
              };
              updateTitle();
              const removeBtn = document.createElement('button');
              removeBtn.type = 'button';
              removeBtn.textContent = '删除';
              removeBtn.addEventListener('click', (event) => {
                event.preventDefault();
                event.stopPropagation();
                section.cards.splice(cardIndex, 1);
                syncJson();
                renderSections(sectionIndex);
                renderHints();
              });
              header.appendChild(dragHandle);
              header.appendChild(title);
              header.appendChild(removeBtn);
              item.appendChild(header);

              const titleLabel = document.createElement('label');
              titleLabel.innerHTML = '<span>标题</span>';
              const titleInput = document.createElement('input');
              titleInput.type = 'text';
              titleInput.value = card.title || '';
              titleInput.addEventListener('input', (event) => {
                card.title = event.target.value;
                updateTitle();
                syncJson();
              });
              titleLabel.appendChild(titleInput);
              item.appendChild(titleLabel);

              const descLabel = document.createElement('label');
              descLabel.innerHTML = '<span>描述</span>';
              const descInput = document.createElement('textarea');
              descInput.rows = 3;
              descInput.value = card.description || '';
              descInput.addEventListener('input', (event) => {
                card.description = event.target.value;
                syncJson();
              });
              descLabel.appendChild(descInput);
              item.appendChild(descLabel);

              wrapper.appendChild(item);
            });
            const addBtn = document.createElement('button');
            addBtn.type = 'button';
            addBtn.textContent = '添加卡片';
            addBtn.addEventListener('click', (event) => {
              event.preventDefault();
              section.cards.push(createCard());
              syncJson();
              renderSections(sectionIndex);
              renderHints();
            });
            const actions = document.createElement('div');
            actions.className = 'sublist-actions';
            actions.appendChild(addBtn);
            const copyCardsBtn = document.createElement('button');
            copyCardsBtn.type = 'button';
            copyCardsBtn.textContent = '复制卡片';
            copyCardsBtn.addEventListener('click', async (event) => {
              event.preventDefault();
              await copySubitems(section.type, section.cards, '暂无卡片可复制', '卡片已复制');
            });
            const pasteCardsBtn = document.createElement('button');
            pasteCardsBtn.type = 'button';
            pasteCardsBtn.textContent = '粘贴卡片';
            pasteCardsBtn.addEventListener('click', async (event) => {
              event.preventDefault();
              await pasteSubitems(section.type, section.cards, normalizeCardEntry, sectionIndex, '卡片已粘贴');
            });
            actions.appendChild(copyCardsBtn);
            actions.appendChild(pasteCardsBtn);
            wrapper.appendChild(actions);
            setupDragSort(wrapper, {
              itemSelector: '.subitem',
              handleSelector: '.drag-handle',
              onMove: (from, to) => {
                if (moveArrayItem(section.cards, from, to)) {
                  syncJson();
                  renderSections(sectionIndex);
                  renderHints();
                }
              }
            });
            break;
          }
          case 'list': {
            section.items = ensureArray(section.items);
            if (!section.items.length) {
              const note = document.createElement('p');
              note.className = 'helper-text';
              note.textContent = '尚未添加列表条目，点击下方按钮新增。';
              wrapper.appendChild(note);
            }
            section.items.forEach((entry, itemIndex) => {
              const item = document.createElement('div');
              item.className = 'subitem';
              const header = document.createElement('header');
              const dragHandle = document.createElement('span');
              dragHandle.className = 'drag-handle';
              dragHandle.textContent = '拖动';
              dragHandle.title = '拖拽排序';
              const title = document.createElement('span');
              title.className = 'subitem-title';
              const updateTitle = () => {
                title.textContent = `条目 ${itemIndex + 1}${entry.title ? ` · ${entry.title}` : ''}`;
              };
              updateTitle();
              const removeBtn = document.createElement('button');
              removeBtn.type = 'button';
              removeBtn.textContent = '删除';
              removeBtn.addEventListener('click', (event) => {
                event.preventDefault();
                section.items.splice(itemIndex, 1);
                syncJson();
                renderSections(sectionIndex);
                renderHints();
              });
              header.appendChild(dragHandle);
              header.appendChild(title);
              header.appendChild(removeBtn);
              item.appendChild(header);

              const titleLabel = document.createElement('label');
              titleLabel.innerHTML = '<span>标题</span>';
              const titleInput = document.createElement('input');
              titleInput.type = 'text';
              titleInput.value = entry.title || '';
              titleInput.addEventListener('input', (event) => {
                entry.title = event.target.value;
                updateTitle();
                syncJson();
              });
              titleLabel.appendChild(titleInput);
              item.appendChild(titleLabel);

              const descLabel = document.createElement('label');
              descLabel.innerHTML = '<span>描述</span>';
              const descInput = document.createElement('textarea');
              descInput.rows = 3;
              descInput.value = entry.description || '';
              descInput.addEventListener('input', (event) => {
                entry.description = event.target.value;
                syncJson();
              });
              descLabel.appendChild(descInput);
              item.appendChild(descLabel);

              wrapper.appendChild(item);
            });
            const addBtn = document.createElement('button');
            addBtn.type = 'button';
            addBtn.textContent = '添加条目';
            addBtn.addEventListener('click', (event) => {
              event.preventDefault();
              section.items.push(createListItem());
              syncJson();
              renderSections(sectionIndex);
              renderHints();
            });
            const actions = document.createElement('div');
            actions.className = 'sublist-actions';
            actions.appendChild(addBtn);
            const copyListBtn = document.createElement('button');
            copyListBtn.type = 'button';
            copyListBtn.textContent = '复制条目';
            copyListBtn.addEventListener('click', async (event) => {
              event.preventDefault();
              await copySubitems(section.type, section.items, '暂无条目可复制', '条目已复制');
            });
            const pasteListBtn = document.createElement('button');
            pasteListBtn.type = 'button';
            pasteListBtn.textContent = '粘贴条目';
            pasteListBtn.addEventListener('click', async (event) => {
              event.preventDefault();
              await pasteSubitems(section.type, section.items, normalizeListEntry, sectionIndex, '条目已粘贴');
            });
            actions.appendChild(copyListBtn);
            actions.appendChild(pasteListBtn);
            wrapper.appendChild(actions);
            setupDragSort(wrapper, {
              itemSelector: '.subitem',
              handleSelector: '.drag-handle',
              onMove: (from, to) => {
                if (moveArrayItem(section.items, from, to)) {
                  syncJson();
                  renderSections(sectionIndex);
                  renderHints();
                }
              }
            });
            break;
          }
          case 'timeline': {
            section.items = ensureArray(section.items);
            if (!section.items.length) {
              const note = document.createElement('p');
              note.className = 'helper-text';
              note.textContent = '尚未添加时间节点，点击下方按钮新增。';
              wrapper.appendChild(note);
            }
            section.items.forEach((entry, itemIndex) => {
              const item = document.createElement('div');
              item.className = 'subitem';
              const header = document.createElement('header');
              const dragHandle = document.createElement('span');
              dragHandle.className = 'drag-handle';
              dragHandle.textContent = '拖动';
              dragHandle.title = '拖拽排序';
              const title = document.createElement('span');
              title.className = 'subitem-title';
              const formatHeader = () => {
                const stamp = tidyString(entry.year) || tidyString(entry.title);
                title.textContent = `节点 ${itemIndex + 1}${stamp ? ` · ${stamp}` : ''}`;
              };
              formatHeader();
              const removeBtn = document.createElement('button');
              removeBtn.type = 'button';
              removeBtn.textContent = '删除';
              removeBtn.addEventListener('click', (event) => {
                event.preventDefault();
                section.items.splice(itemIndex, 1);
                syncJson();
                renderSections(sectionIndex);
                renderHints();
              });
              header.appendChild(dragHandle);
              header.appendChild(title);
              header.appendChild(removeBtn);
              item.appendChild(header);

              const yearLabel = document.createElement('label');
              yearLabel.innerHTML = '<span>年份 / 时间点</span>';
              const yearInput = document.createElement('input');
              yearInput.type = 'text';
              yearInput.value = entry.year || '';
              yearInput.addEventListener('input', (event) => {
                entry.year = event.target.value;
                formatHeader();
                syncJson();
              });
              yearLabel.appendChild(yearInput);
              item.appendChild(yearLabel);

              const titleLabel = document.createElement('label');
              titleLabel.innerHTML = '<span>标题</span>';
              const titleInput = document.createElement('input');
              titleInput.type = 'text';
              titleInput.value = entry.title || '';
              titleInput.addEventListener('input', (event) => {
                entry.title = event.target.value;
                formatHeader();
                syncJson();
              });
              titleLabel.appendChild(titleInput);
              item.appendChild(titleLabel);

              const descLabel = document.createElement('label');
              descLabel.innerHTML = '<span>描述</span>';
              const descInput = document.createElement('textarea');
              descInput.rows = 3;
              descInput.value = entry.description || '';
              descInput.addEventListener('input', (event) => {
                entry.description = event.target.value;
                syncJson();
              });
              descLabel.appendChild(descInput);
              item.appendChild(descLabel);

              wrapper.appendChild(item);
            });
            const addBtn = document.createElement('button');
            addBtn.type = 'button';
            addBtn.textContent = '添加节点';
            addBtn.addEventListener('click', (event) => {
              event.preventDefault();
              section.items.push(createTimelineItem());
              syncJson();
              renderSections(sectionIndex);
              renderHints();
            });
            const actions = document.createElement('div');
            actions.className = 'sublist-actions';
            actions.appendChild(addBtn);
            const copyTimelineBtn = document.createElement('button');
            copyTimelineBtn.type = 'button';
            copyTimelineBtn.textContent = '复制节点';
            copyTimelineBtn.addEventListener('click', async (event) => {
              event.preventDefault();
              await copySubitems(section.type, section.items, '暂无节点可复制', '时间节点已复制');
            });
            const pasteTimelineBtn = document.createElement('button');
            pasteTimelineBtn.type = 'button';
            pasteTimelineBtn.textContent = '粘贴节点';
            pasteTimelineBtn.addEventListener('click', async (event) => {
              event.preventDefault();
              await pasteSubitems(section.type, section.items, normalizeTimelineEntry, sectionIndex, '时间节点已粘贴');
            });
            actions.appendChild(copyTimelineBtn);
            actions.appendChild(pasteTimelineBtn);
            wrapper.appendChild(actions);
            setupDragSort(wrapper, {
              itemSelector: '.subitem',
              handleSelector: '.drag-handle',
              onMove: (from, to) => {
                if (moveArrayItem(section.items, from, to)) {
                  syncJson();
                  renderSections(sectionIndex);
                  renderHints();
                }
              }
            });
            break;
          }
          case 'contact': {
            section.items = ensureArray(section.items);
            if (!section.items.length) {
              const note = document.createElement('p');
              note.className = 'helper-text';
              note.textContent = '尚未添加联系方式，点击下方按钮新增。';
              wrapper.appendChild(note);
            }
            section.items.forEach((entry, entryIndex) => {
              const item = document.createElement('div');
              item.className = 'subitem';
              const header = document.createElement('header');
              const dragHandle = document.createElement('span');
              dragHandle.className = 'drag-handle';
              dragHandle.textContent = '拖动';
              dragHandle.title = '拖拽排序';
              const title = document.createElement('span');
              title.className = 'subitem-title';
              const updateTitle = () => {
                title.textContent = `条目 ${entryIndex + 1}${entry.label ? ` · ${entry.label}` : ''}`;
              };
              updateTitle();
              const removeBtn = document.createElement('button');
              removeBtn.type = 'button';
              removeBtn.textContent = '删除';
              removeBtn.addEventListener('click', (event) => {
                event.preventDefault();
                section.items.splice(entryIndex, 1);
                syncJson();
                renderSections(sectionIndex);
                renderHints();
              });
              header.appendChild(dragHandle);
              header.appendChild(title);
              header.appendChild(removeBtn);
              item.appendChild(header);

              const labelLabel = document.createElement('label');
              labelLabel.innerHTML = '<span>标签</span>';
              const labelInput = document.createElement('input');
              labelInput.type = 'text';
              labelInput.value = entry.label || '';
              labelInput.addEventListener('input', (event) => {
                entry.label = event.target.value;
                updateTitle();
                syncJson();
              });
              labelLabel.appendChild(labelInput);
              item.appendChild(labelLabel);

              const valueLabel = document.createElement('label');
              valueLabel.innerHTML = '<span>内容</span>';
              const valueInput = document.createElement('textarea');
              valueInput.rows = 3;
              valueInput.value = entry.value || '';
              valueInput.addEventListener('input', (event) => {
                entry.value = event.target.value;
                syncJson();
              });
              valueLabel.appendChild(valueInput);
              item.appendChild(valueLabel);

              wrapper.appendChild(item);
            });
            const addBtn = document.createElement('button');
            addBtn.type = 'button';
            addBtn.textContent = '添加联系方式';
            addBtn.addEventListener('click', (event) => {
              event.preventDefault();
              section.items.push(createContactItem());
              syncJson();
              renderSections(sectionIndex);
              renderHints();
            });
            const actions = document.createElement('div');
            actions.className = 'sublist-actions';
            actions.appendChild(addBtn);
            const copyContactBtn = document.createElement('button');
            copyContactBtn.type = 'button';
            copyContactBtn.textContent = '复制联系方式';
            copyContactBtn.addEventListener('click', async (event) => {
              event.preventDefault();
              await copySubitems(section.type, section.items, '暂无联系方式可复制', '联系方式已复制');
            });
            const pasteContactBtn = document.createElement('button');
            pasteContactBtn.type = 'button';
            pasteContactBtn.textContent = '粘贴联系方式';
            pasteContactBtn.addEventListener('click', async (event) => {
              event.preventDefault();
              await pasteSubitems(section.type, section.items, normalizeContactEntry, sectionIndex, '联系方式已粘贴');
            });
            actions.appendChild(copyContactBtn);
            actions.appendChild(pasteContactBtn);
            wrapper.appendChild(actions);
            setupDragSort(wrapper, {
              itemSelector: '.subitem',
              handleSelector: '.drag-handle',
              onMove: (from, to) => {
                if (moveArrayItem(section.items, from, to)) {
                  syncJson();
                  renderSections(sectionIndex);
                  renderHints();
                }
              }
            });
            break;
          }
          case 'stats': {
            section.stats = ensureArray(section.stats);
            if (!section.stats.length) {
              const note = document.createElement('p');
              note.className = 'helper-text';
              note.textContent = '尚未添加指标，点击下方按钮新增。';
              wrapper.appendChild(note);
            }
            section.stats.forEach((entry, statIndex) => {
              const item = document.createElement('div');
              item.className = 'subitem';
              const header = document.createElement('header');
              const dragHandle = document.createElement('span');
              dragHandle.className = 'drag-handle';
              dragHandle.textContent = '拖动';
              dragHandle.title = '拖拽排序';
              const title = document.createElement('span');
              title.className = 'subitem-title';
              const formatHeader = () => {
                const label = tidyString(entry.label) || tidyString(entry.value);
                title.textContent = `指标 ${statIndex + 1}${label ? ` · ${label}` : ''}`;
              };
              formatHeader();
              const removeBtn = document.createElement('button');
              removeBtn.type = 'button';
              removeBtn.textContent = '删除';
              removeBtn.addEventListener('click', (event) => {
                event.preventDefault();
                section.stats.splice(statIndex, 1);
                syncJson();
                renderSections(sectionIndex);
                renderHints();
              });
              header.appendChild(dragHandle);
              header.appendChild(title);
              header.appendChild(removeBtn);
              item.appendChild(header);

              const valueLabel = document.createElement('label');
              valueLabel.innerHTML = '<span>数值</span>';
              const valueInput = document.createElement('input');
              valueInput.type = 'text';
              valueInput.value = entry.value || '';
              valueInput.addEventListener('input', (event) => {
                entry.value = event.target.value;
                formatHeader();
                syncJson();
              });
              valueLabel.appendChild(valueInput);
              item.appendChild(valueLabel);

              const labelLabel = document.createElement('label');
              labelLabel.innerHTML = '<span>标签</span>';
              const labelInput = document.createElement('input');
              labelInput.type = 'text';
              labelInput.value = entry.label || '';
              labelInput.addEventListener('input', (event) => {
                entry.label = event.target.value;
                formatHeader();
                syncJson();
              });
              labelLabel.appendChild(labelInput);
              item.appendChild(labelLabel);

              const descLabel = document.createElement('label');
              descLabel.innerHTML = '<span>描述（可选）</span>';
              const descInput = document.createElement('textarea');
              descInput.rows = 3;
              descInput.value = entry.description || '';
              descInput.addEventListener('input', (event) => {
                entry.description = event.target.value;
                syncJson();
              });
              descLabel.appendChild(descInput);
              item.appendChild(descLabel);

              wrapper.appendChild(item);
            });
            const addBtn = document.createElement('button');
            addBtn.type = 'button';
            addBtn.textContent = '添加指标';
            addBtn.addEventListener('click', (event) => {
              event.preventDefault();
              section.stats.push(createStat());
              syncJson();
              renderSections(sectionIndex);
              renderHints();
            });
            const actions = document.createElement('div');
            actions.className = 'sublist-actions';
            actions.appendChild(addBtn);
            const copyStatsBtn = document.createElement('button');
            copyStatsBtn.type = 'button';
            copyStatsBtn.textContent = '复制指标';
            copyStatsBtn.addEventListener('click', async (event) => {
              event.preventDefault();
              await copySubitems(section.type, section.stats, '暂无指标可复制', '指标已复制');
            });
            const pasteStatsBtn = document.createElement('button');
            pasteStatsBtn.type = 'button';
            pasteStatsBtn.textContent = '粘贴指标';
            pasteStatsBtn.addEventListener('click', async (event) => {
              event.preventDefault();
              await pasteSubitems(section.type, section.stats, normalizeStatEntry, sectionIndex, '指标已粘贴');
            });
            actions.appendChild(copyStatsBtn);
            actions.appendChild(pasteStatsBtn);
            wrapper.appendChild(actions);
            setupDragSort(wrapper, {
              itemSelector: '.subitem',
              handleSelector: '.drag-handle',
              onMove: (from, to) => {
                if (moveArrayItem(section.stats, from, to)) {
                  syncJson();
                  renderSections(sectionIndex);
                  renderHints();
                }
              }
            });
            break;
          }
          case 'gallery': {
            section.items = ensureArray(section.items);
            if (!section.items.length) {
              const note = document.createElement('p');
              note.className = 'helper-text';
              note.textContent = '尚未添加图片，点击下方按钮新增。';
              wrapper.appendChild(note);
            }
            section.items.forEach((image, imageIndex) => {
              const item = document.createElement('div');
              item.className = 'subitem';
              const header = document.createElement('header');
              const dragHandle = document.createElement('span');
              dragHandle.className = 'drag-handle';
              dragHandle.textContent = '拖动';
              dragHandle.title = '拖拽排序';
              const title = document.createElement('span');
              title.className = 'subitem-title';
              const updateTitle = () => {
                title.textContent = `图片 ${imageIndex + 1}${image.alt ? ` · ${image.alt}` : ''}`;
              };
              updateTitle();
              const removeBtn = document.createElement('button');
              removeBtn.type = 'button';
              removeBtn.textContent = '删除';
              removeBtn.addEventListener('click', (event) => {
                event.preventDefault();
                section.items.splice(imageIndex, 1);
                syncJson();
                renderSections(sectionIndex);
                renderHints();
              });
              header.appendChild(dragHandle);
              header.appendChild(title);
              header.appendChild(removeBtn);
              item.appendChild(header);

              const srcLabel = document.createElement('label');
              srcLabel.innerHTML = '<span>图片地址</span>';
              const srcInput = document.createElement('input');
              srcInput.type = 'text';
              srcInput.value = image.src || '';
              srcInput.addEventListener('input', (event) => {
                image.src = event.target.value;
                syncJson();
              });
              srcLabel.appendChild(srcInput);
              item.appendChild(srcLabel);

              const altLabel = document.createElement('label');
              altLabel.innerHTML = '<span>替换文本</span>';
              const altInput = document.createElement('input');
              altInput.type = 'text';
              altInput.value = image.alt || '';
              altInput.addEventListener('input', (event) => {
                image.alt = event.target.value;
                updateTitle();
                syncJson();
              });
              altLabel.appendChild(altInput);
              item.appendChild(altLabel);

              const captionLabel = document.createElement('label');
              captionLabel.innerHTML = '<span>说明（可选）</span>';
              const captionInput = document.createElement('textarea');
              captionInput.rows = 2;
              captionInput.value = image.caption || '';
              captionInput.addEventListener('input', (event) => {
                image.caption = event.target.value;
                syncJson();
              });
              captionLabel.appendChild(captionInput);
              item.appendChild(captionLabel);

              wrapper.appendChild(item);
            });
            const addBtn = document.createElement('button');
            addBtn.type = 'button';
            addBtn.textContent = '添加图片';
            addBtn.addEventListener('click', (event) => {
              event.preventDefault();
              section.items.push(createGalleryItem());
              syncJson();
              renderSections(sectionIndex);
              renderHints();
            });
            const actions = document.createElement('div');
            actions.className = 'sublist-actions';
            actions.appendChild(addBtn);
            const copyGalleryBtn = document.createElement('button');
            copyGalleryBtn.type = 'button';
            copyGalleryBtn.textContent = '复制图片';
            copyGalleryBtn.addEventListener('click', async (event) => {
              event.preventDefault();
              await copySubitems(section.type, section.items, '暂无图片可复制', '图片已复制');
            });
            const pasteGalleryBtn = document.createElement('button');
            pasteGalleryBtn.type = 'button';
            pasteGalleryBtn.textContent = '粘贴图片';
            pasteGalleryBtn.addEventListener('click', async (event) => {
              event.preventDefault();
              await pasteSubitems(section.type, section.items, normalizeGalleryItem, sectionIndex, '图片已粘贴');
            });
            actions.appendChild(copyGalleryBtn);
            actions.appendChild(pasteGalleryBtn);
            wrapper.appendChild(actions);
            setupDragSort(wrapper, {
              itemSelector: '.subitem',
              handleSelector: '.drag-handle',
              onMove: (from, to) => {
                if (moveArrayItem(section.items, from, to)) {
                  syncJson();
                  renderSections(sectionIndex);
                  renderHints();
                }
              }
            });
            break;
          }
          case 'richtext':
          case 'basic': {
            const label = document.createElement('label');
            label.innerHTML = '<span>富文本内容（支持 HTML）</span>';
            const field = document.createElement('textarea');
            field.rows = 6;
            field.value = section.content || '';
            field.addEventListener('input', (event) => {
              section.content = event.target.value;
              syncJson();
              renderHints();
            });
            label.appendChild(field);
            wrapper.appendChild(label);
            break;
          }
          default: {
            const note = document.createElement('p');
            note.className = 'helper-text';
            note.textContent = '该模块类型暂未适配可视化编辑，请使用 JSON 模式调整。';
            wrapper.appendChild(note);
            break;
          }
        }

        return wrapper;
      }

      function renderSections(openIndex) {
        sections = draft.sections = ensureArray(draft.sections).map((entry) => normalizeSection(entry));
        sectionsList.innerHTML = '';
        if (!sections.length) {
          sectionEmpty.classList.remove('hidden');
          return;
        }
        sectionEmpty.classList.add('hidden');
        if (typeof openIndex === 'number') {
          lastOpenIndex = Math.max(0, Math.min(openIndex, sections.length - 1));
        } else {
          lastOpenIndex = Math.max(0, Math.min(lastOpenIndex, sections.length - 1));
        }
        sections.forEach((section, index) => {
          const details = document.createElement('details');
          details.className = 'section-item';
          details.open = index === lastOpenIndex;
          details.addEventListener('toggle', () => {
            if (details.open) {
              lastOpenIndex = index;
            }
          });

          const summary = document.createElement('summary');
          const handle = document.createElement('span');
          handle.className = 'section-handle';
          handle.textContent = index + 1;
          const titleSpan = document.createElement('span');
          titleSpan.className = 'section-summary-title';
          titleSpan.textContent = section.title || `模块 ${index + 1}`;
          const typeTag = document.createElement('span');
          typeTag.className = 'section-type-tag';
          typeTag.textContent = sectionTypeLabels[section.type] || section.type || 'basic';

          const btnGroup = document.createElement('div');
          btnGroup.className = 'section-buttons';
          const upBtn = document.createElement('button');
          upBtn.type = 'button';
          upBtn.textContent = '上移';
          if (index === 0) upBtn.disabled = true;
          upBtn.addEventListener('click', (event) => {
            event.preventDefault();
            event.stopPropagation();
            moveSection(index, -1);
          });
          const downBtn = document.createElement('button');
          downBtn.type = 'button';
          downBtn.textContent = '下移';
          if (index === sections.length - 1) downBtn.disabled = true;
          downBtn.addEventListener('click', (event) => {
            event.preventDefault();
            event.stopPropagation();
            moveSection(index, 1);
          });
          const copyBtn = document.createElement('button');
          copyBtn.type = 'button';
          copyBtn.textContent = '复制';
          copyBtn.addEventListener('click', async (event) => {
            event.preventDefault();
            event.stopPropagation();
            await copySectionAt(index);
          });
          const pasteBtn = document.createElement('button');
          pasteBtn.type = 'button';
          pasteBtn.textContent = '粘贴';
          pasteBtn.addEventListener('click', async (event) => {
            event.preventDefault();
            event.stopPropagation();
            await pasteSectionAt(index + 1);
          });
          const removeBtn = document.createElement('button');
          removeBtn.type = 'button';
          removeBtn.textContent = '删除';
          removeBtn.addEventListener('click', (event) => {
            event.preventDefault();
            event.stopPropagation();
            if (sections.length === 1 || confirm('确认删除该内容模块？')) {
              removeSection(index);
            }
          });
          btnGroup.appendChild(upBtn);
          btnGroup.appendChild(downBtn);
          btnGroup.appendChild(copyBtn);
          btnGroup.appendChild(pasteBtn);
          btnGroup.appendChild(removeBtn);

          summary.appendChild(handle);
          summary.appendChild(titleSpan);
          summary.appendChild(typeTag);
          summary.appendChild(btnGroup);
          details.appendChild(summary);

          const fields = document.createElement('div');
          fields.className = 'section-fields';

          const topGrid = document.createElement('div');
          topGrid.className = 'field-grid split';

          const idLabel = document.createElement('label');
          idLabel.innerHTML = '<span>模块 ID</span>';
          const idInput = document.createElement('input');
          idInput.type = 'text';
          idInput.value = section.id || '';
          idInput.addEventListener('input', (event) => {
            section.id = event.target.value.trim();
            syncJson();
          });
          idLabel.appendChild(idInput);
          topGrid.appendChild(idLabel);

          const typeLabel = document.createElement('label');
          typeLabel.innerHTML = '<span>模块类型</span>';
          const typeSelect = document.createElement('select');
          sectionTypeOrder.forEach((typeKey) => {
            const option = document.createElement('option');
            option.value = typeKey;
            option.textContent = sectionTypeLabels[typeKey];
            typeSelect.appendChild(option);
          });
          typeSelect.value = section.type || 'cards';
          typeSelect.addEventListener('change', (event) => {
            transformSection(section, event.target.value);
            syncJson();
            renderSections(index);
            renderHints();
          });
          typeLabel.appendChild(typeSelect);
          topGrid.appendChild(typeLabel);

          const titleLabel = document.createElement('label');
          titleLabel.innerHTML = '<span>模块标题</span>';
          const titleInput = document.createElement('input');
          titleInput.type = 'text';
          titleInput.value = section.title || '';
          titleInput.addEventListener('input', (event) => {
            section.title = event.target.value;
            titleSpan.textContent = section.title.trim() || `模块 ${index + 1}`;
            syncJson();
            renderHints();
          });
          titleLabel.appendChild(titleInput);
          topGrid.appendChild(titleLabel);

          fields.appendChild(topGrid);

          const descLabel = document.createElement('label');
          descLabel.innerHTML = '<span>模块描述</span>';
          const descInput = document.createElement('textarea');
          descInput.rows = 3;
          descInput.value = section.description || '';
          descInput.addEventListener('input', (event) => {
            section.description = event.target.value;
            syncJson();
          });
          descLabel.appendChild(descInput);
          fields.appendChild(descLabel);

          const ctaWrapper = document.createElement('div');
          ctaWrapper.className = 'inline-inputs';
          const ctaLabel = document.createElement('label');
          ctaLabel.innerHTML = '<span>CTA 文案</span>';
          const ctaLabelInput = document.createElement('input');
          ctaLabelInput.type = 'text';
          ctaLabelInput.value = section.cta?.label || '';
          const ctaHrefLabel = document.createElement('label');
          ctaHrefLabel.innerHTML = '<span>CTA 链接</span>';
          const ctaHrefInput = document.createElement('input');
          ctaHrefInput.type = 'text';
          ctaHrefInput.value = section.cta?.href || '';
          const applySectionCta = () => {
            const labelValue = tidyString(ctaLabelInput.value);
            const hrefValue = tidyString(ctaHrefInput.value);
            section.cta =
              labelValue || hrefValue
                ? { label: labelValue, href: hrefValue, target: /^https?:\/\//i.test(hrefValue) ? '_blank' : '_self' }
                : null;
            syncJson();
          };
          ctaLabelInput.addEventListener('input', applySectionCta);
          ctaHrefInput.addEventListener('input', applySectionCta);
          ctaLabel.appendChild(ctaLabelInput);
          ctaHrefLabel.appendChild(ctaHrefInput);
          ctaWrapper.appendChild(ctaLabel);
          ctaWrapper.appendChild(ctaHrefLabel);
          fields.appendChild(ctaWrapper);

          const tintLabel = document.createElement('label');
          tintLabel.style.display = 'flex';
          tintLabel.style.alignItems = 'center';
          tintLabel.style.gap = '0.5rem';
          const tintInput = document.createElement('input');
          tintInput.type = 'checkbox';
          tintInput.checked = !!section.tint;
          tintInput.addEventListener('change', (event) => {
            section.tint = event.target.checked;
            syncJson();
          });
          const tintText = document.createElement('span');
          tintText.textContent = '浅色背景（Tint）';
          tintLabel.appendChild(tintInput);
          tintLabel.appendChild(tintText);
          fields.appendChild(tintLabel);

          fields.appendChild(buildSectionSpecificFields(section, index));

          details.appendChild(fields);
          sectionsList.appendChild(details);
        });
        setupDragSort(sectionsList, {
          itemSelector: '.section-item',
          handleSelector: '.section-handle',
          onMove: (from, to) => {
            moveSectionTo(from, to);
          },
          hoverClass: 'drag-over'
        });
      }

      function reload(nextDoc) {
        doc = preparePageDocument(nextDoc);
        state.pages[key] = doc;
        meta = doc.meta;
        draft = doc.draft;
        hero = draft.hero;
        sections = draft.sections;
        populateMeta();
        populateHero();
        renderSections();
        syncJson();
        return renderHints();
      }

      function applyJsonFromTextarea() {
        const parsed = JSON.parse(textarea.value);
        return reload(parsed);
      }

      metaTitle.addEventListener('input', (event) => {
        meta.title = event.target.value;
        syncJson();
        renderHints();
      });
      metaSummary.addEventListener('input', (event) => {
        meta.summary = event.target.value;
        syncJson();
        renderHints();
      });
      metaHeroImage.addEventListener('input', (event) => {
        meta.heroImage = tidyString(event.target.value);
        syncJson();
      });
      metaLocale.addEventListener('input', (event) => {
        meta.locale = tidyString(event.target.value) || 'zh-CN';
        syncJson();
      });
      metaTags.addEventListener('input', (event) => {
        meta.tags = parseLines(event.target.value);
        syncJson();
      });
      metaCtaLabel.addEventListener('input', () => {
        updateMetaCta();
        syncJson();
      });
      metaCtaHref.addEventListener('input', () => {
        updateMetaCta();
        syncJson();
      });

      heroEyebrow.addEventListener('input', (event) => {
        hero.eyebrow = event.target.value;
        syncJson();
      });
      heroBadge.addEventListener('input', (event) => {
        hero.badge = event.target.value;
        syncJson();
      });
      heroTitle.addEventListener('input', (event) => {
        hero.title = event.target.value;
        syncJson();
        renderHints();
      });
      heroSubtitle.addEventListener('input', (event) => {
        hero.subtitle = event.target.value;
        syncJson();
      });
      heroLead.addEventListener('input', (event) => {
        hero.lead = event.target.value;
        syncJson();
        renderHints();
      });
      heroDescription.addEventListener('input', (event) => {
        hero.description = event.target.value;
        syncJson();
      });
      heroCtaLabel.addEventListener('input', () => {
        updateHeroCta();
        syncJson();
      });
      heroCtaHref.addEventListener('input', () => {
        updateHeroCta();
        syncJson();
      });
      heroMediaType.addEventListener('change', () => {
        updateHeroMedia();
        syncJson();
      });
      heroMediaSrc.addEventListener('input', () => {
        updateHeroMedia();
        syncJson();
      });
      heroMediaAlt.addEventListener('input', () => {
        updateHeroMedia();
        syncJson();
      });
      heroMeta.addEventListener('input', (event) => {
        hero.meta = parseLines(event.target.value);
        syncJson();
      });
      heroMetrics.addEventListener('input', (event) => {
        hero.metrics = parseMetrics(event.target.value);
        syncJson();
      });

      addSectionBtn.addEventListener('click', (event) => {
        event.preventDefault();
        const type = sectionTypeSelect.value || 'cards';
        const newSection = createSection(type);
        newSection.id = `${type}-${sections.length + 1}`;
        sections.push(newSection);
        lastOpenIndex = sections.length - 1;
        syncJson();
        renderSections(lastOpenIndex);
        renderHints();
      });

      pasteSectionBtn?.addEventListener('click', async (event) => {
        event.preventDefault();
        await pasteSectionAt(sections.length);
      });

      populateMeta();
      populateHero();
      renderSections();
      syncJson();
      renderHints();

      return {
        sync: syncJson,
        validate: () => renderHints(),
        reload,
        applyJson: applyJsonFromTextarea,
        getDoc: () => doc
      };
    }

    const defaultTheme = {
      palette: {
        primary: '#165DFF',
        accent: '#FF7D00',
        neutral: '#E6E9EF'
      },
      surfaces: {
        page: '#0B0C10',
        nav: '#101F40',
        cube: '#1E40AF',
        gradients: {
          page: { enabled: false, from: '#165DFF', to: '#101F40', angle: 135 },
          nav: { enabled: false, from: '#101F40', to: '#165DFF', angle: 135 },
          cube: { enabled: false, from: '#1E40AF', to: '#0B0C10', angle: 150 }
        }
      },
      text: {
        heading: '#E6E9EF',
        body: '#C6CCDA',
        inverse: '#0B0C10',
        interactive: '#165DFF'
      }
    };

    const defaultGradients = {
      hero: {
        enabled: true,
        angle: 135,
        from: '#2B63FF',
        to: '#1232A4'
      }
    };

    function normalizeHex(value) {
      const hex = (value || '').trim();
      if (!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(hex)) return null;
      const raw = hex.slice(1).toUpperCase();
      if (raw.length === 3) {
        return `#${raw[0]}${raw[0]}${raw[1]}${raw[1]}${raw[2]}${raw[2]}`;
      }
      return `#${raw}`;
    }

    function normalizeAngleValue(value) {
      if (value === null || value === undefined || value === '') return null;
      const num = Number(value);
      if (!Number.isFinite(num)) return null;
      const normalized = Math.round(num) % 360;
      return normalized >= 0 ? normalized : normalized + 360;
    }

    function clampAngle(value, fallback = 135) {
      const normalized = normalizeAngleValue(value);
      return normalized === null ? fallback : normalized;
    }

    function ensureGradientEntry(entry, defaults) {
      const base = {
        enabled: Boolean(defaults && defaults.enabled),
        from: normalizeHex(defaults && defaults.from) || '#165DFF',
        to: normalizeHex(defaults && defaults.to) || '#101F40',
        angle: clampAngle(defaults && defaults.angle, 135)
      };
      if (!entry || typeof entry !== 'object') {
        return base;
      }
      let enabled = Boolean(entry.enabled);
      if (typeof entry.enabled === 'string') {
        const lowered = entry.enabled.trim().toLowerCase();
        if (['false', '0', 'no', 'off'].includes(lowered)) {
          enabled = false;
        } else if (['true', '1', 'yes', 'on'].includes(lowered)) {
          enabled = true;
        }
      } else if (typeof entry.enabled === 'boolean') {
        enabled = entry.enabled;
      }
      return {
        enabled,
        from: normalizeHex(entry.from) || base.from,
        to: normalizeHex(entry.to) || base.to,
        angle: clampAngle(entry.angle, base.angle)
      };
    }

    function ensureTheme() {
      if (!state.site) return;
      if (!state.site.theme || typeof state.site.theme !== 'object') {
        state.site.theme = JSON.parse(JSON.stringify(defaultTheme));
      }
      for (const groupKey of Object.keys(defaultTheme)) {
        const defaults = defaultTheme[groupKey];
        const target = state.site.theme[groupKey];
        if (groupKey === 'palette' && target) {
          const legacySecondary = normalizeHex(target.secondary);
          if (legacySecondary && !normalizeHex(target.accent)) {
            target.accent = legacySecondary;
          }
        }
        if (groupKey === 'text' && target) {
          const legacyButton = normalizeHex(target.button);
          if (legacyButton && !normalizeHex(target.inverse)) {
            target.inverse = legacyButton;
          }
        }
        if (!target || typeof target !== 'object') {
          state.site.theme[groupKey] = JSON.parse(JSON.stringify(defaults));
          continue;
        }
        if (groupKey === 'palette') {
          delete target.secondary;
        }
        if (groupKey === 'text') {
          delete target.button;
        }
        for (const fieldKey of Object.keys(defaults)) {
          if (groupKey === 'surfaces' && fieldKey === 'gradients') {
            continue;
          }
          const defaultValue = defaults[fieldKey];
          if (typeof defaultValue === 'object' && defaultValue !== null) {
            continue;
          }
          const existing = normalizeHex(target[fieldKey]);
          state.site.theme[groupKey][fieldKey] = existing || defaultValue;
        }
        if (groupKey === 'surfaces') {
          const gradientDefaults = defaults.gradients || {};
          const gradientsTarget =
            target.gradients && typeof target.gradients === 'object' ? target.gradients : {};
          const normalizedGradients = {};
          Object.keys(gradientDefaults).forEach((key) => {
            normalizedGradients[key] = ensureGradientEntry(gradientsTarget[key], gradientDefaults[key]);
          });
          state.site.theme[groupKey].gradients = normalizedGradients;
        }
      }
      if (!state.site.theme.gradients || typeof state.site.theme.gradients !== 'object') {
        state.site.theme.gradients = JSON.parse(JSON.stringify(defaultGradients));
      }
      const gradients = state.site.theme.gradients;
      if (!gradients.hero || typeof gradients.hero !== 'object') {
        gradients.hero = JSON.parse(JSON.stringify(defaultGradients.hero));
      }
      const heroGradient = gradients.hero;
      const enabledRaw = heroGradient.enabled;
      if (typeof enabledRaw === 'string') {
        const lowered = enabledRaw.trim().toLowerCase();
        if (['false', '0', 'no', 'off'].includes(lowered)) {
          heroGradient.enabled = false;
        } else if (['true', '1', 'yes', 'on'].includes(lowered)) {
          heroGradient.enabled = true;
        } else {
          heroGradient.enabled = defaultGradients.hero.enabled;
        }
      } else if (typeof enabledRaw === 'boolean') {
        heroGradient.enabled = enabledRaw;
      } else {
        heroGradient.enabled = defaultGradients.hero.enabled;
      }
      heroGradient.from = normalizeHex(heroGradient.from) || defaultGradients.hero.from;
      heroGradient.to = normalizeHex(heroGradient.to) || defaultGradients.hero.to;
      let angle = Number(heroGradient.angle);
      if (!Number.isFinite(angle)) angle = defaultGradients.hero.angle;
      angle = Math.round(angle);
      if (!Number.isFinite(angle)) angle = defaultGradients.hero.angle;
      angle %= 360;
      if (angle < 0) angle += 360;
      heroGradient.angle = angle;
    }

    function getDefaultThemeValue(path) {
      if (path.startsWith('gradients.')) {
        const parts = path.split('.').slice(1);
        return parts.reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : null), defaultGradients);
      }
      return path.split('.').reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : null), defaultTheme);
    }

    function getThemeValue(path) {
      ensureTheme();
      if (!state.site || !state.site.theme) return null;
      return path.split('.').reduce((acc, key) => {
        if (acc === null || acc === undefined) return null;
        if (typeof acc !== 'object') return null;
        return acc[key];
      }, state.site.theme);
    }

    function setThemeValue(path, value) {
      const normalized = normalizeHex(value);
      if (!normalized) return;
      ensureTheme();
      if (!state.site) return;
      const parts = path.split('.');
      let cursor = state.site.theme;
      for (let i = 0; i < parts.length - 1; i += 1) {
        const key = parts[i];
        if (!cursor[key] || typeof cursor[key] !== 'object') {
          cursor[key] = {};
        }
        cursor = cursor[key];
      }
      cursor[parts[parts.length - 1]] = normalized;
    }

    function setThemeRawValue(path, value) {
      ensureTheme();
      if (!state.site) return;
      const parts = path.split('.');
      let cursor = state.site.theme;
      for (let i = 0; i < parts.length - 1; i += 1) {
        const key = parts[i];
        if (!cursor[key] || typeof cursor[key] !== 'object') {
          cursor[key] = {};
        }
        cursor = cursor[key];
      }
      cursor[parts[parts.length - 1]] = value;
    }

    function setControlValue(control, value) {
      const fallback = getDefaultThemeValue(control.path);
      const normalized = normalizeHex(value) || normalizeHex(fallback);
      if (!normalized) return;
      if (control.picker && control.picker.value !== normalized) {
        control.picker.value = normalized;
      }
      if (control.field) {
        control.field.value = normalized;
        control.field.classList.remove('is-invalid');
      }
      setThemeValue(control.path, normalized);
    }

    function ensureHeroGradient() {
      ensureTheme();
      if (!state.site || !state.site.theme) return null;
      if (!state.site.theme.gradients || typeof state.site.theme.gradients !== 'object') {
        state.site.theme.gradients = JSON.parse(JSON.stringify(defaultGradients));
      }
      if (!state.site.theme.gradients.hero || typeof state.site.theme.gradients.hero !== 'object') {
        state.site.theme.gradients.hero = JSON.parse(JSON.stringify(defaultGradients.hero));
      }
      return state.site.theme.gradients.hero;
    }

    function updateGradientFieldsState(enabled) {
      if (!els.gradientFieldWrappers) return;
      els.gradientFieldWrappers.forEach((wrapper) => {
        wrapper.classList.toggle('is-disabled', !enabled);
        wrapper.setAttribute('aria-disabled', String(!enabled));
        wrapper.querySelectorAll('input').forEach((input) => {
          if (input.type !== 'checkbox') {
            input.disabled = !enabled;
          }
        });
      });
    }

    function renderGradientSettings() {
      const hero = ensureHeroGradient();
      if (!hero) return;
      if (els.gradientEnabled) {
        els.gradientEnabled.checked = !!hero.enabled;
      }
      if (els.gradientAngle) {
        els.gradientAngle.value =
          hero.angle !== undefined && hero.angle !== null ? hero.angle : defaultGradients.hero.angle;
      }
      updateGradientFieldsState(!!hero.enabled);
    }

    function syncGradientSettings() {
      const hero = ensureHeroGradient();
      if (!hero) return true;
      if (els.gradientEnabled) {
        hero.enabled = !!els.gradientEnabled.checked;
      }
      if (els.gradientAngle) {
        let angle = Number(els.gradientAngle.value);
        if (!Number.isFinite(angle)) angle = defaultGradients.hero.angle;
        angle = Math.round(angle);
        angle %= 360;
        if (angle < 0) angle += 360;
        hero.angle = angle;
        els.gradientAngle.value = angle;
      }
      updateGradientFieldsState(!!hero.enabled);
      return true;
    }

    const themeControls = Array.from(document.querySelectorAll('[data-theme-key]')).map((element) => {
      const control = {
        element,
        path: element.dataset.themeKey,
        picker: element.querySelector('.color-picker'),
        field: element.querySelector('.color-value')
      };
      if (control.picker) {
        control.picker.addEventListener('input', () => {
          setControlValue(control, control.picker.value);
        });
      }
      if (control.field) {
        control.field.addEventListener('input', () => {
          const normalized = normalizeHex(control.field.value);
          if (normalized) {
            control.field.value = normalized;
            control.field.classList.remove('is-invalid');
            setControlValue(control, normalized);
          } else {
            control.field.classList.add('is-invalid');
          }
        });
        control.field.addEventListener('blur', () => {
          const current = getThemeValue(control.path) || getDefaultThemeValue(control.path);
          setControlValue(control, current);
        });
      }
      return control;
    });

    const gradientItems = Array.from(document.querySelectorAll('[data-gradient-key]')).map((container) => {
      const key = container.dataset.gradientKey;
      const basePath = `surfaces.gradients.${key}`;
      const toggle = container.querySelector('[data-gradient-toggle]');
      const angleInput = container.querySelector('[data-gradient-angle]');
      const colorPickers = Array.from(container.querySelectorAll('.color-picker'));
      const colorFields = Array.from(container.querySelectorAll('.color-value'));
      const entry = {
        key,
        basePath,
        container,
        toggle,
        angleInput,
        colorPickers,
        colorFields
      };
      if (toggle) {
        toggle.addEventListener('change', () => {
          setThemeRawValue(`${basePath}.enabled`, toggle.checked);
          updateGradientItemState(entry, toggle.checked);
        });
      }
      if (angleInput) {
        angleInput.addEventListener('input', () => angleInput.classList.remove('is-invalid'));
        angleInput.addEventListener('blur', () => {
          const normalized = normalizeAngleValue(angleInput.value);
          if (normalized === null) {
            angleInput.classList.add('is-invalid');
            const current = getThemeValue(`${basePath}.angle`);
            if (typeof current === 'number') {
              angleInput.value = current;
            }
            return;
          }
          angleInput.value = normalized;
          setThemeRawValue(`${basePath}.angle`, normalized);
        });
      }
      return entry;
    });

    function updateGradientItemState(entry, enabledOverride) {
      ensureTheme();
      const enabled =
        typeof enabledOverride === 'boolean'
          ? enabledOverride
          : Boolean(getThemeValue(`${entry.basePath}.enabled`));
      entry.container.classList.toggle('is-disabled', !enabled);
      entry.container.setAttribute('aria-disabled', String(!enabled));
      if (entry.toggle) {
        entry.toggle.checked = enabled;
      }
      const inputs = [...entry.colorPickers, ...entry.colorFields];
      inputs.forEach((input) => {
        input.disabled = !enabled;
        if (!enabled && input.classList) {
          input.classList.remove('is-invalid');
        }
      });
      if (entry.angleInput) {
        entry.angleInput.disabled = !enabled;
        if (!enabled) {
          entry.angleInput.classList.remove('is-invalid');
        }
      }
    }

    function renderSurfaceGradients() {
      ensureTheme();
      gradientItems.forEach((entry) => {
        const defaults = defaultTheme.surfaces.gradients?.[entry.key] || {};
        const enabled = Boolean(getThemeValue(`${entry.basePath}.enabled`));
        if (entry.angleInput) {
          const angleValue = getThemeValue(`${entry.basePath}.angle`);
          const angle =
            typeof angleValue === 'number'
              ? clampAngle(angleValue, defaults.angle || 135)
              : defaults.angle || 135;
          entry.angleInput.value = angle;
          entry.angleInput.classList.remove('is-invalid');
        }
        updateGradientItemState(entry, enabled);
      });
    }

    function syncGradientAngles() {
      ensureTheme();
      let valid = true;
      gradientItems.forEach((entry) => {
        if (!entry.angleInput) return;
        const defaults = defaultTheme.surfaces.gradients?.[entry.key] || {};
        const enabled = Boolean(getThemeValue(`${entry.basePath}.enabled`));
        const normalized = normalizeAngleValue(entry.angleInput.value);
        if (normalized === null) {
          if (!enabled) {
            const fallback = defaults.angle !== undefined ? clampAngle(defaults.angle, 135) : 135;
            entry.angleInput.classList.remove('is-invalid');
            entry.angleInput.value = fallback;
            setThemeRawValue(`${entry.basePath}.angle`, fallback);
            return;
          }
          entry.angleInput.classList.add('is-invalid');
          valid = false;
          return;
        }
        entry.angleInput.classList.remove('is-invalid');
        entry.angleInput.value = normalized;
        setThemeRawValue(`${entry.basePath}.angle`, normalized);
      });
      return valid;
    }

    function renderTheme() {
      ensureTheme();
      themeControls.forEach((control) => {
        const value = getThemeValue(control.path) || getDefaultThemeValue(control.path);
        setControlValue(control, value);
      });
      renderSurfaceGradients();
      renderGradientSettings();
    }

    function syncThemeValues() {
      ensureTheme();
      let valid = true;
      themeControls.forEach((control) => {
        if (!control.field) return;
        const normalized = normalizeHex(control.field.value);
        if (normalized) {
          setControlValue(control, normalized);
        } else {
          control.field.classList.add('is-invalid');
          valid = false;
        }
      });
      const heroValid = syncGradientSettings();
      const gradientAnglesValid = syncGradientAngles();
      return valid && heroValid && gradientAnglesValid;
    }

    function loadToken() {
      const saved = localStorage.getItem('wg-admin-token');
      if (saved) {
        state.token = saved;
        els.token.value = saved;
      }
    }

    function rememberToken() {
      state.token = els.token.value.trim();
      if (!state.token) {
        alert('请输入令牌');
        return;
      }
      localStorage.setItem('wg-admin-token', state.token);
      alert('已保存令牌');
    }

    async function request(url, options = {}) {
      const headers = Object.assign({ 'Content-Type': 'application/json' }, options.headers);
      if (state.token) headers['x-admin-token'] = state.token;
      const res = await fetch(url, Object.assign({}, options, { headers }));
      const bodyText = await res.text();
      if (!res.ok) {
        let message = res.statusText;
        let payload = null;
        if (bodyText) {
          try {
            payload = JSON.parse(bodyText);
            message = payload?.error || message;
          } catch (err) {
            message = bodyText;
          }
        }
        const error = new Error(message || '请求失败');
        error.status = res.status;
        if (payload) {
          error.payload = payload;
        }
        throw error;
      }
      if (!bodyText) return null;
      try {
        return JSON.parse(bodyText);
      } catch (err) {
        throw new Error('响应解析失败');
      }
    }

    function renderNav() {
      els.navList.innerHTML = '';
      (state.site.navigation || []).forEach((item, index) => {
      const tpl = document.getElementById('navItemTemplate').content.cloneNode(true);
      tpl.querySelector('.nav-label').textContent = `导航 ${index + 1}`;
      tpl.querySelector('.nav-key').value = item.key || '';
      tpl.querySelector('.nav-text').value = item.label || '';
      tpl.querySelector('.nav-route').value = item.route || '';
      const pageRefInput = tpl.querySelector('.nav-page-ref');
      if (pageRefInput) pageRefInput.value = item.pageRef || '';
      const summaryInput = tpl.querySelector('.nav-summary');
      if (summaryInput) summaryInput.value = item.summary || '';
      tpl.querySelector('.btn-remove-nav').addEventListener('click', () => {
        state.site.navigation.splice(index, 1);
        renderNav();
      });
      els.navList.appendChild(tpl);
      });
    }

    function renderFaces() {
      els.faceList.innerHTML = '';
      (state.cube.faces || []).forEach((face, index) => {
      const tpl = document.getElementById('faceTemplate').content.cloneNode(true);
      tpl.querySelector('.face-title').textContent = `面 ${index + 1} · ${face.key}`;
      tpl.querySelector('.face-label').value = face.label || '';
      tpl.querySelector('.face-subtitle').value = face.subtitle || '';
      tpl.querySelector('.face-description').value = face.description || '';
      tpl.querySelector('.face-page-ref').value = face.pageRef || '';
      tpl.querySelector('.face-summary').value = face.summary || '';
      tpl.querySelector('.face-cta-label').value = face.cta?.label || '';
      tpl.querySelector('.face-cta-href').value = face.cta?.href || '';
      tpl.querySelector('.face-route').value = face.route || '';
      tpl.querySelector('.face-color-from').value = face.colors?.[0] || '';
      tpl.querySelector('.face-color-to').value = face.colors?.[1] || '';
        const preview = tpl.querySelector('.face-preview');
        const info = tpl.querySelector('.face-image-path');
        if (face.image) {
          const img = document.createElement('img');
          img.src = resolveAssetUrl(face.image);
          img.style.width = '100%';
          img.style.height = '100%';
          img.style.objectFit = 'cover';
          preview.innerHTML = '';
          preview.appendChild(img);
          info.textContent = face.image;
        } else {
          preview.innerHTML = '<span class="muted">未设置图片</span>';
          info.textContent = '';
        }
        tpl.querySelector('.btn-reset-image').addEventListener('click', () => {
          state.cube.faces[index].image = null;
          renderFaces();
        });
        const fileInput = tpl.querySelector('.face-file');
        tpl.querySelector('.btn-upload-image').addEventListener('click', async () => {
          if (!fileInput.files.length) {
            alert('请选择图片文件');
            return;
          }
          const file = fileInput.files[0];
          if (file.size > 5 * 1024 * 1024) {
            alert('图片请小于 5MB');
            return;
          }
          try {
            const base64 = await fileToBase64(file);
            const payload = {
              filename: `${face.key || 'face'}-${Date.now()}`,
              data: base64.split(',').pop(),
              extension: pathExt(file.name)
            };
            const result = await request(api.upload, {
              method: 'POST',
              body: JSON.stringify(payload)
            });
            state.cube.faces[index].image = result.path;
            fileInput.value = '';
            renderFaces();
            els.cubeStatus.textContent = '图片已上传';
            setTimeout(() => (els.cubeStatus.textContent = ''), 2000);
          } catch (err) {
            console.error(err);
            alert('上传失败：' + err.message);
          }
        });
        els.faceList.appendChild(tpl);
      });
    }

    function renderPages() {
      els.pagesList.innerHTML = '';
      pageKeys.forEach((key) => {
        const tpl = document.getElementById('pageEditorTemplate').content.cloneNode(true);
        const titleMap = {
          about: '关于我们',
          services: '业务',
          products: '互动',
          solutions: '解决方案/案例',
          team: '团队',
          contact: '联系我们'
        };
        const pageTitle = titleMap[key] || key;
        const container = tpl.querySelector('.page-editor');
        const textarea = tpl.querySelector('.page-json');
        const status = tpl.querySelector('.page-status');
        const btnSave = tpl.querySelector('.btn-save-page');
        const btnSync = tpl.querySelector('.btn-sync-json');
        const btnDraft = tpl.querySelector('.btn-save-draft');
        const btnPublish = tpl.querySelector('.btn-publish');
        const btnPreview = tpl.querySelector('.btn-preview');
        const previewBanner = tpl.querySelector('.preview-banner');
        const previewMessage = tpl.querySelector('.preview-message');
        const previewUrlEl = tpl.querySelector('.preview-url');
        const btnCopyPreview = tpl.querySelector('.btn-copy-preview');

        tpl.querySelector('.page-title').textContent = `${pageTitle}（${key}）`;

        const pageForm = initPageForm(container, key, textarea, status);

        let latestPreviewUrl = '';

        const resetPreviewBanner = () => {
          latestPreviewUrl = '';
          if (previewMessage) previewMessage.textContent = '';
          if (previewUrlEl) previewUrlEl.textContent = '';
          if (previewBanner) previewBanner.classList.add('hidden');
          if (btnCopyPreview) btnCopyPreview.disabled = true;
        };

        resetPreviewBanner();

        if (pageForm && typeof pageForm.reload === 'function') {
          const originalReload = pageForm.reload;
          pageForm.reload = (nextDoc) => {
            resetPreviewBanner();
            return originalReload(nextDoc);
          };
        }

        const showStatus = (message) => {
          status.textContent = message;
          setTimeout(() => (status.textContent = ''), 2400);
        };

        btnCopyPreview?.addEventListener('click', async (event) => {
          event.preventDefault();
          if (!latestPreviewUrl) {
            alert('请先生成预览链接');
            return;
          }
          try {
            if (navigator.clipboard?.writeText) {
              await navigator.clipboard.writeText(latestPreviewUrl);
              showStatus('预览链接已复制');
              return;
            }
            throw new Error('Clipboard API not available');
          } catch (err) {
            try {
              const helper = document.createElement('textarea');
              helper.value = latestPreviewUrl;
              helper.setAttribute('readonly', '');
              helper.style.position = 'fixed';
              helper.style.opacity = '0';
              document.body.appendChild(helper);
              helper.select();
              document.execCommand('copy');
              document.body.removeChild(helper);
              showStatus('预览链接已复制');
            } catch (fallbackErr) {
              window.prompt('复制以下链接', latestPreviewUrl);
            }
          }
        });

        const persist = async (mutator, hint, { syncForm = true, checkWarnings = false } = {}) => {
          state.token = els.token.value.trim();
          if (!state.token) {
            alert('请先输入管理员令牌');
            return;
          }
          let warnings = [];
          if (syncForm) {
            pageForm.sync();
            warnings = pageForm.validate();
            if (checkWarnings && warnings.length) {
              const proceed = confirm(`检测到以下提示：\n- ${warnings.join('\n- ')}\n仍要继续执行操作吗？`);
              if (!proceed) {
                return;
              }
            }
          }
          let payload;
          try {
            payload = JSON.parse(textarea.value);
          } catch (err) {
            alert('JSON 解析失败，请检查格式：' + err.message);
            return;
          }
          if (typeof mutator === 'function') {
            try {
              mutator(payload);
            } catch (err) {
              console.error(err);
            }
          }
          try {
            await request(`/api/content/pages/${key}`, {
              method: 'PUT',
              body: JSON.stringify(payload)
            });
            const fresh = await request(`/api/content/pages/${key}`, {
              method: 'GET',
              headers: { 'Cache-Control': 'no-cache' }
            });
            state.pages[key] = fresh || {};
            pageForm.reload(state.pages[key]);
            showStatus(hint);
          } catch (err) {
            const schemaDetails =
              err?.payload && Array.isArray(err.payload.details) && err.payload.details.length
                ? `\n- ${err.payload.details.join('\n- ')}`
                : '';
            alert(`保存失败：${err.message}${schemaDetails}`);
          }
        };

        btnSave?.addEventListener('click', () =>
          persist(null, '已保存', { syncForm: false, checkWarnings: false })
        );

        btnSync?.addEventListener('click', () => {
          try {
            pageForm.applyJson();
            resetPreviewBanner();
            showStatus('已同步 JSON 至表单');
          } catch (err) {
            alert('解析 JSON 失败：' + err.message);
          }
        });

        btnDraft?.addEventListener('click', () =>
          persist(
            (doc) => {
              doc.status = 'draft';
              if (!doc.draft && doc.published) {
                doc.draft = deepClone(doc.published);
              }
            },
            '草稿已保存',
            { syncForm: true, checkWarnings: true }
          )
        );

        btnPublish?.addEventListener('click', () =>
          persist(
            (doc) => {
              doc.status = 'published';
              if (!doc.published && doc.draft) {
                doc.published = deepClone(doc.draft);
              }
            },
            '已发布',
            { syncForm: true, checkWarnings: true }
          )
        );

        btnPreview?.addEventListener('click', async () => {
          state.token = els.token.value.trim();
          if (!state.token) {
            alert('请先输入管理员令牌');
            return;
          }
          pageForm.sync();
          try {
            const payload = JSON.parse(textarea.value);
            const result = await request(`/api/content/pages/${key}/preview`, {
              method: 'POST',
              body: JSON.stringify(payload)
            });
            if (!result || !result.token) {
              throw new Error('预览令牌生成失败');
            }
            const previewPath = `/pages/${key}.html?preview=${encodeURIComponent(result.token)}`;
            const previewUrl = (() => {
              try {
                return new URL(previewPath, window.location.origin).toString();
              } catch (err) {
                return previewPath;
              }
            })();
            latestPreviewUrl = previewUrl;
            if (previewUrlEl) {
              previewUrlEl.textContent = previewUrl;
            }
            if (btnCopyPreview) {
              btnCopyPreview.disabled = false;
            }
            if (previewBanner) {
              previewBanner.classList.remove('hidden');
            }
            if (previewMessage) {
              const expiryInfo = getPreviewExpiryInfo(result.expiresAt);
              if (expiryInfo) {
                previewMessage.innerHTML = `草稿预览链接已生成，将在 <strong>${expiryInfo.relative}</strong> 失效（截止 ${expiryInfo.absolute}）。`;
              } else {
                previewMessage.textContent = '草稿预览链接已生成。';
              }
            }
            window.open(previewPath, '_blank');
            showStatus('预览已打开');
          } catch (err) {
            const schemaDetails =
              err?.payload && Array.isArray(err.payload.details) && err.payload.details.length
                ? `\n- ${err.payload.details.join('\n- ')}`
                : '';
            alert(`预览失败：${err.message}${schemaDetails}`);
          }
        });

        els.pagesList.appendChild(container);
      });
    }

    function syncNavValues() {
    const items = els.navList.querySelectorAll('.nav-item, .card');
    state.site.navigation = Array.from(items).map((node) => ({
      key: node.querySelector('.nav-key').value.trim(),
      label: node.querySelector('.nav-text').value.trim(),
      route: node.querySelector('.nav-route').value.trim(),
      pageRef: (node.querySelector('.nav-page-ref')?.value || '').trim(),
      summary: (node.querySelector('.nav-summary')?.value || '').trim()
    }));
  }

    function syncFaceValues() {
    const cards = els.faceList.querySelectorAll('.card');
    state.cube.faces = Array.from(cards).map((card, index) => {
      const original = state.cube.faces[index] || {};
      const ctaLabel = card.querySelector('.face-cta-label').value.trim();
      const ctaHref = card.querySelector('.face-cta-href').value.trim();
      return Object.assign({}, original, {
        label: card.querySelector('.face-label').value.trim(),
        subtitle: card.querySelector('.face-subtitle').value.trim(),
        description: card.querySelector('.face-description').value.trim(),
        pageRef: card.querySelector('.face-page-ref').value.trim(),
        summary: card.querySelector('.face-summary').value.trim(),
        route: card.querySelector('.face-route').value.trim(),
        colors: [
          card.querySelector('.face-color-from').value.trim(),
          card.querySelector('.face-color-to').value.trim()
        ],
        cta: ctaLabel || ctaHref ? { label: ctaLabel, href: ctaHref, target: /^https?:\/\//i.test(ctaHref) ? '_blank' : '_self' } : null
      });
    });
  }

    async function loadData() {
      try {
        await ensurePageSchema();
        state.site = await request(api.site, { method: 'GET', headers: { 'Cache-Control': 'no-cache' } });
        state.cube = await request(api.cube, { method: 'GET', headers: { 'Cache-Control': 'no-cache' } });
        state.pages = {};
        for (const key of pageKeys) {
          try {
            state.pages[key] = await request(`/api/content/pages/${key}`, { method: 'GET', headers: { 'Cache-Control': 'no-cache' } });
          } catch (err) {
            console.warn(`加载 ${key} 页面失败`, err);
            state.pages[key] = {};
          }
        }
        els.company.value = state.site.companyName || '';
        els.homeRoute.value = state.site.homeRoute || '/';
        els.icp.value = state.site.icpRecord || '';
        els.hint.value = state.site.adminTokenHint || '';
        renderNav();
        renderTheme();
        renderFaces();
        renderPages();
      } catch (err) {
        console.error(err);
        alert('加载数据失败：' + err.message);
      }
    }

    async function saveSite() {
      state.token = els.token.value.trim();
      if (!state.token) {
        alert('请先输入管理员令牌');
        return;
      }
      const themeValid = syncThemeValues();
      if (!themeValid) {
        alert('请检查色彩或渐变配置字段，需使用 #RGB/#RRGGBB 颜色与 0-359° 角度');
        return;
      }
      syncNavValues();
      state.site.companyName = els.company.value.trim();
      state.site.homeRoute = els.homeRoute.value.trim() || '/';
      state.site.icpRecord = els.icp.value.trim();
      state.site.adminTokenHint = els.hint.value.trim();
      try {
        await request(api.site, {
          method: 'PUT',
          body: JSON.stringify(state.site)
        });
        const targets = [els.siteStatus, els.themeStatus];
        targets
          .filter((el) => el)
          .forEach((el) => {
            el.textContent = '已保存';
            setTimeout(() => {
              el.textContent = '';
            }, 2000);
          });
      } catch (err) {
        alert('保存失败：' + err.message);
      }
    }

    async function saveCube() {
      state.token = els.token.value.trim();
      if (!state.token) {
        alert('请先输入管理员令牌');
        return;
      }
      syncFaceValues();
      try {
        await request(api.cube, {
          method: 'PUT',
          body: JSON.stringify(state.cube)
        });
        els.cubeStatus.textContent = '已保存';
        setTimeout(() => (els.cubeStatus.textContent = ''), 2000);
      } catch (err) {
        alert('保存失败：' + err.message);
      }
    }

    function addNavItem() {
      state.site.navigation.push({ key: '', label: '', route: '' });
      renderNav();
    }

    function fileToBase64(file) {
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => resolve(reader.result);
        reader.onerror = reject;
        reader.readAsDataURL(file);
      });
    }

    function pathExt(name) {
      const idx = name.lastIndexOf('.');
      return idx >= 0 ? name.slice(idx) : '.png';
    }

    function resolveAssetUrl(value) {
      if (!value) return '';
      const str = String(value).trim();
      if (!str) return '';
      if (/^https?:\/\//i.test(str)) return str;
      return `/${str.replace(/^\/+/, '')}`;
    }

    els.remember.addEventListener('click', rememberToken);
    els.addNav.addEventListener('click', addNavItem);
    if (els.saveSite) {
      els.saveSite.addEventListener('click', saveSite);
    }
    if (els.saveTheme) {
      els.saveTheme.addEventListener('click', saveSite);
    }
    els.saveCube.addEventListener('click', saveCube);
    if (els.gradientEnabled) {
      els.gradientEnabled.addEventListener('change', () => {
        syncGradientSettings();
      });
    }
    if (els.gradientAngle) {
      const onGradientAngleChange = () => {
        syncGradientSettings();
      };
      els.gradientAngle.addEventListener('input', onGradientAngleChange);
      els.gradientAngle.addEventListener('blur', onGradientAngleChange);
    }

    loadToken();
    loadData();
  </script>
</body>
</html>
